diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..d49b37f5d54a5147a80f8fd16508a1563a98cb20 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +*.sh text eol=lf +*.sql text eol=lf +*.spec text eol=lf +*.example text eol=lf +*.go text eol=lf +.gitattributes text eol=lf +scripts/repository-node-rpm/* text eol=lf +manager/backend/services/footstone/configs/application.yaml.template text eol=lf diff --git a/.gitignore b/.gitignore index 78c7fdaabffb6edfbecfa48db712c4aefdc1c48f..88148d299b0008db9c5c3d86f7c9cb968490fe8a 100644 --- a/.gitignore +++ b/.gitignore @@ -67,6 +67,8 @@ manager/backend/**/trpc_go.yaml Thumbs.db *.swp .run/ +.playwright-cli/ +output/ # ========================= # 本地 AI 代理产物(不入库) diff --git a/README.md b/README.md index 472c54e11c780f3f68b76f6f33c950dbae522865..211c589a9ec0814e39a5116ac07fa602875f8ec1 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ OpenCloudOS智能管家,由 **Vue 3 + Vite 前端**、**Go 后端**(`manager | 系统诊断 | 命令助手(远程执行,下游对接数据通道) | | 运维任务编排 | `tms/` **数据通道**:agent 接入、资产/证书、心跳判活、任务下发、物料分发(详见 [docs/architecture/tms-channel.md](docs/architecture/tms-channel.md)) | | OCAI 智能问答 | 控制台内嵌 AI 助手前端入口,对接外部 AI 推理服务(详见 [docs/architecture/frontend-app.md](docs/architecture/frontend-app.md#ocai-智能问答)) | +| 私有化软件仓库 | 按产品版本管理同步仓和私有 RPM,支持独立仓库节点、repodata、GPG、隔离 DNF 验证、分发和 OCAI 失败分析 | | 用户与审计 | 本地账号/JWT 登录、操作记录、浏览日志 | ## 仓库结构 @@ -29,10 +30,10 @@ ocmanager/ │ └── reference/ # 目录详细参考 ├── scripts/ # 构建辅助脚本(含一键部署,覆盖 manager + tms) ├── examples/ # 示例与占位 +├── config/ +│ └── env.example # 根目录环境变量样例(复制为 config/.env) ├── manager/ # 产品主体(前端 + Go 后端 monorepo) │ ├── Makefile # 统一构建入口 -│ ├── config/ -│ │ └── env.example # 环境变量样例(复制为 .env 使用) │ ├── deployments/ │ │ └── compose/ │ │ └── docker-compose.yml # manager 业务(footstone / msg-etl / sysdiagnose / frontend) @@ -69,7 +70,7 @@ ocmanager/ **前置依赖**: -- **Docker 20.10+**(含 Compose v2)+ **Go 1.24+**(`deploy.sh` 会在本机静态编译后端二进制,再 COPY 进镜像) +- **Docker 20.10+**(含 Compose v2)+ **Go 1.25+**(`deploy.sh` 会在本机静态编译后端二进制,再 COPY 进镜像) - 当前用户能**直接调用 docker**(root 或在 `docker` 组里) - 当前用户能 **sudo**(首次部署时若需自动安装系统依赖会用到) @@ -137,7 +138,7 @@ bash scripts/deploy.sh tms down # 仅停数据通道(tms/) ### 方式二:本地开发(make + Vite Dev Server) -**前置依赖**:Go 1.24+、Node.js 18+、Docker 20.10+、**gcc**(编译 `tms/file-svc` 的 CGO/sqlite)。 +**前置依赖**:Go 1.25+、Node.js 20+、Docker 20.10+、**gcc**(编译 `tms/file-svc` 的 CGO/sqlite)。 **配置关系**: @@ -223,12 +224,14 @@ bash scripts/deploy.sh tms restart asset-manager gateway #### 2. 打包并安装 Agent ```bash -# Agent 仓(构建机,需能读到控制面的 ca.crt) -GATEWAY_ADDR=<对外地址>:8989 bash scripts/build-tar.sh # 同机验证可用 127.0.0.1:8989 - -# 目标主机 -tar -xzf ocm-agent-linux-amd64.tar.gz -C /tmp/ocm-agent-install && cd /tmp/ocm-agent-install -sudo bash install.sh +# Agent 仓(构建机,需能读到控制面的 ca.crt;替换示例域名) +GATEWAY_ADDR="management.example:8989" bash scripts/build-tar.sh + +# 目标主机:先在 OCManager 源码根目录规范化配置,再安装 +export AGENT_PACKAGE_DIR="$(mktemp -d)" +tar -xzf ocm-agent-linux-amd64.tar.gz -C "$AGENT_PACKAGE_DIR" +bash scripts/configure-agent-package.sh "$AGENT_PACKAGE_DIR" "management.example:8989" +(cd "$AGENT_PACKAGE_DIR" && sudo bash install.sh) systemctl status ocm-agent ``` @@ -239,7 +242,7 @@ systemctl status ocm-agent | 场景 | 做法 | |------|------| | **PoC / 单机 / 内网**(推荐) | 编辑 `tms/deploy/configs/asset-manager-trpc_go.yaml`,设 `auto_register: true`,然后 `bash scripts/deploy.sh tms restart asset-manager`,再 `systemctl restart ocm-agent` | -| **生产 / 需审批** | 从 Agent 日志 `fingerprint detected` 取 uuid/ip/mac,调用 `POST /api/v1/assets/add`(需 `X-User-Name` 头)加入白名单 | +| **生产 / 需审批** | 从 Agent 日志 `fingerprint detected` 取 uuid/ip/mac,通过 `tms/deploy/scripts/gw-sign.sh` 调用 `POST /api/v1/assets/add` 加入白名单 | 日志出现 `registration rejected: agent not in whitelist` 即未登记——详见 [FAQ §5](docs/FAQ.md#5-agent-安装后离线类问题排查思路) 与 [TROUBLESHOOTING](tms/deploy/docs/TROUBLESHOOTING.md)。 @@ -251,11 +254,11 @@ API **没有** `/api/v1/agents/list`,用下面两条(`API_HTTP_PORT` 默认 API=http://127.0.0.1:8080 # 已注册资产 -curl -sS -X POST "$API/api/v1/assets/query" \ +bash tms/deploy/scripts/gw-sign.sh -X POST "$API/api/v1/assets/query" \ -H 'Content-Type: application/json' -d '{"agent_id": 0}' | jq # 在线状态(agent_id 换成上一步返回值) -curl -sS -X POST "$API/api/v1/agents/status" \ +bash tms/deploy/scripts/gw-sign.sh -X POST "$API/api/v1/agents/status" \ -H 'Content-Type: application/json' \ -d '{"agent_ids": []}' | jq ``` @@ -306,6 +309,10 @@ curl -sS -X POST "$API/api/v1/agents/status" \ | 文档 | 说明 | |------|------| | [docs/architecture/](docs/architecture/) | 系统上下文、后端分层、前端定位、命令助手设计、数据流时序 | +| [docs/私有化软件仓库方案设计.md](docs/私有化软件仓库方案设计.md) | 私有化软件仓库总体方案、系统架构和关键技术设计 | +| [docs/私有化软件仓库部署说明.md](docs/私有化软件仓库部署说明.md) | 管理面、仓库节点、TMS、OCAI 的部署和恢复步骤 | +| [docs/私有化软件仓库测试验收.md](docs/私有化软件仓库测试验收.md) | Release Acceptance 入口、判定标准和现场演示清单 | +| [docs/私有化软件仓库结构化验收报告.md](docs/私有化软件仓库结构化验收报告.md) | 实际实现、接口、前端操作、截图和结构化验收结果 | | [docs/reference/database-schema.md](docs/reference/database-schema.md) | **数据库表结构 + ER 图** | | [docs/reference/api-reference.md](docs/reference/api-reference.md) | **API / Kafka 消息总览** | | [docs/reference/env-variables.md](docs/reference/env-variables.md) | 环境变量参考(按服务/用途索引) | diff --git a/config/env.example b/config/env.example index 650dfa014297ebe4be7c7e463e8fb6a2b7b48a40..05609672d51cf5ef4ac3a79592b3e6d4a6dc3b8d 100644 --- a/config/env.example +++ b/config/env.example @@ -223,6 +223,83 @@ NGINX_HOST=localhost # ================================ LOG_DIR=./logs +# ================================ +# 私有化软件仓库管理面与独立 Repository Storage Node +# ================================ +# 以下 bind mount 仅供独立仓库节点使用。Footstone 和管理面 Nginx 不挂载此目录。 +REPOSITORY_DATA_HOST_DIR=/opt/ocmanager/repository-data +REPOSITORY_DATA_ROOT=/var/lib/ocmanager/repository-data +# Footstone 节点内部 RPC。证书由部署环境提供;开发环境未准备 PKI 时保持关闭。 +REPOSITORY_NODE_INTERNAL_ENABLED=false +REPOSITORY_NODE_INTERNAL_LISTEN_ADDRESS=0.0.0.0:19443 +REPOSITORY_NODE_INTERNAL_PORT=19443 +REPOSITORY_NODE_INTERNAL_PUBLIC_TARGET= +REPOSITORY_NODE_INTERNAL_SERVER_CERT=/etc/ocmanager/repository-node-pki/server.crt +REPOSITORY_NODE_INTERNAL_SERVER_KEY=/etc/ocmanager/repository-node-pki/server.key +REPOSITORY_NODE_INTERNAL_SERVER_CA=/etc/ocmanager/repository-node-pki/ca.crt +REPOSITORY_NODE_INTERNAL_SERVER_NAME= +REPOSITORY_NODE_INTERNAL_CLIENT_CA=/etc/ocmanager/repository-node-pki/ca.crt +REPOSITORY_NODE_BOOTSTRAP_ENABLED=false +REPOSITORY_NODE_BOOTSTRAP_LISTEN_ADDRESS=0.0.0.0:19444 +REPOSITORY_NODE_BOOTSTRAP_PORT=19444 +REPOSITORY_NODE_BOOTSTRAP_PUBLIC_TARGET= +REPOSITORY_NODE_BOOTSTRAP_SERVER_CERT=/etc/ocmanager/repository-node-pki/server.crt +REPOSITORY_NODE_BOOTSTRAP_SERVER_KEY=/etc/ocmanager/repository-node-pki/server.key +REPOSITORY_NODE_BOOTSTRAP_SERVER_CA=/etc/ocmanager/repository-node-pki/ca.crt +REPOSITORY_NODE_BOOTSTRAP_SERVER_NAME= +REPOSITORY_NODE_SIGNING_PKI_HOST_DIR=/etc/ocmanager/repository-node-signing +REPOSITORY_NODE_ARTIFACT_HOST_DIR=./repository-node-artifacts +# 只允许部署机上的私有目录,不要指向 repository-data 或 Git 工作区。 +REPOSITORY_NODE_PKI_HOST_DIR=/etc/ocmanager/repository-node-pki +REPOSITORY_NODE_ID= +# 必须使用 ip://host:port;传输仍由下方 CA、客户端证书和 server name 执行 mTLS 校验。 +REPOSITORY_NODE_CONTROL_TARGET= +REPOSITORY_NODE_CLIENT_CERT=/etc/ocmanager/repository-node-pki/client.crt +REPOSITORY_NODE_CLIENT_KEY=/etc/ocmanager/repository-node-pki/client.key +REPOSITORY_NODE_SERVER_CA=/etc/ocmanager/repository-node-pki/ca.crt +# 必须与 Footstone 控制面服务端证书的 SAN 完全一致。 +REPOSITORY_NODE_SERVER_NAME= +REPOSITORY_NODE_HEARTBEAT_SECONDS=10 +REPOSITORY_NODE_RPC_TIMEOUT_SECONDS=10 +# Repository Storage Node 上的反向 mTLS 数据接口;与 Footstone 可使用同一私有 CA,但证书角色分开。 +REPOSITORY_NODE_DATA_SERVER_ENABLED=false +REPOSITORY_NODE_DATA_SERVER_LISTEN_ADDRESS=0.0.0.0:19443 +REPOSITORY_NODE_DATA_SERVER_PORT=19443 +REPOSITORY_NODE_DATA_SERVER_CERT=/etc/ocmanager/repository-node-pki/node-server.crt +REPOSITORY_NODE_DATA_SERVER_KEY=/etc/ocmanager/repository-node-pki/node-server.key +REPOSITORY_NODE_DATA_SERVER_CLIENT_CA=/etc/ocmanager/repository-node-pki/ca.crt +REPOSITORY_NODE_DATA_SERVER_READ_TIMEOUT_SECONDS=30 +# Footstone 读取节点任务结果时使用的客户端证书;私钥不得位于 repository-data。 +REPOSITORY_NODE_DATA_CLIENT_ENABLED=false +REPOSITORY_NODE_DATA_CLIENT_CERT=/etc/ocmanager/repository-node-pki/footstone-client.crt +REPOSITORY_NODE_DATA_CLIENT_KEY=/etc/ocmanager/repository-node-pki/footstone-client.key +REPOSITORY_NODE_DATA_CLIENT_SERVER_CA=/etc/ocmanager/repository-node-pki/ca.crt +REPOSITORY_NODE_DATA_CLIENT_TIMEOUT_SECONDS=30 +REPOSITORY_NODE_MAX_RESULT_BYTES=67108864 +# 客户端可访问的 Repository HTTP 根路径;远端部署必须把 localhost 改为服务地址。 +# 未配置或地址不合法时,产品拒绝生成 .repo 文件。 +REPOSITORY_PUBLIC_BASE_URL= +REPOSITORY_PUBLIC_PORT=18090 +REPOSITORY_TMS_USER_NAME=ocmanager-repository +REPOSITORY_MAX_UPLOAD_BYTES=2147483648 +REPOSITORY_MAX_CHUNK_BYTES=4194304 +REPOSITORY_MAX_ARCHIVE_FILES=1000 +REPOSITORY_MAX_EXPANDED_BYTES=8589934592 +# 全量同步与筛选同步使用分级预算;管理员配置是最终硬上限。 +REPOSITORY_MAX_SYNC_BYTES=68719476736 +REPOSITORY_MAX_SYNC_PACKAGES=100000 +REPOSITORY_FILTERED_SYNC_BYTES=2147483648 +REPOSITORY_FILTERED_SYNC_PACKAGES=1000 +REPOSITORY_METADATA_RESERVE_BYTES=536870912 +REPOSITORY_UPLOAD_TTL_HOURS=24 +REPOSITORY_MINIMUM_FREE_BYTES=1073741824 +REPOSITORY_METADATA_SIGNING_KEY_ROOT=/etc/ocmanager/repository-metadata-signing +REPOSITORY_METADATA_SIGNING_KEY_HOST_DIR=/etc/ocmanager/repository-metadata-signing +REPOSITORY_WORKER_LEASE_SECONDS=60 +REPOSITORY_WORKER_POLL_MILLISECONDS=1000 +# Repository worker 必须使用带 rpm、dnf reposync 与 createrepo_c 的 OpenCloudOS 9 运行时。 +REPOSITORY_RUNTIME_IMAGE=docker.io/opencloudos/opencloudos9-minimal:9.6-v20260714 + NGINX_VERSION=1.24.0 MYSQL_VERSION=8.0.35 REDIS_VERSION=7.2.3 @@ -231,32 +308,34 @@ ZOOKEEPER_VERSION=3.8.3 CLICKHOUSE_VERSION=23.8.8.20 # ================================ -# 镜像源与基镜像覆盖(可选;默认经 mirror.ccs.tencentyun.com,仅腾讯云内网可达) -# 非腾讯云环境(外部开发机 / 海外)可切换到 docker.io 或自建仓库: +# 镜像源与基镜像覆盖。默认使用公开 registry;隔离网可改为已同步的内网 registry。 # ================================ # 基础设施镜像仓库前缀(mysql / redis / zookeeper / kafka / clickhouse) -#OCM_IMAGE_MIRROR=docker.io +OCM_IMAGE_MIRROR=docker.io # bitnami 官方目录已于 2025 年迁移,Docker Hub 上历史 tag 位于 bitnamilegacy/ 下; # 使用 OCM_IMAGE_MIRROR=docker.io 时需一并启用以下两项: -#ZOOKEEPER_REPO=bitnamilegacy/zookeeper -#KAFKA_REPO=bitnamilegacy/kafka +ZOOKEEPER_REPO=bitnamilegacy/zookeeper +KAFKA_REPO=bitnamilegacy/kafka # tms / manager 构建基镜像(Go 编译、运行时、前端 node/nginx) #GO_IMAGE=docker.io/library/golang:1.25-alpine +# Footstone 的 Repository 依赖预览需要 dnf/repoquery,默认使用 OpenCloudOS 9 运行时。 +#FOOTSTONE_RUNTIME_IMAGE=docker.io/opencloudos/opencloudos9-minimal:9.6-v20260714 #RUNTIME_IMAGE=docker.io/library/alpine:3.19 # CGO 服务(task-manager / asset-manager / file-svc)build 时 apk 安装 gcc 的镜像源; -# 默认 mirrors.tencentyun.com(腾讯云 CVM 内网,与 mirror.ccs.tencentyun.com 同体系); -# 非腾讯云或内网不可达时可改为 mirrors.cloud.tencent.com / mirrors.aliyun.com / dl-cdn.alpinelinux.org -#APK_MIRROR=mirrors.tencentyun.com -#RUNTIME_IMAGE=docker.io/library/alpine:3.19 -#NODE_IMAGE=docker.io/library/node:18-alpine +# 默认使用可公网解析的 mirrors.cloud.tencent.com;隔离网可改为已同步的内网镜像, +# 也可改为 mirrors.aliyun.com / dl-cdn.alpinelinux.org。 +#APK_MIRROR=mirrors.cloud.tencent.com +#NODE_IMAGE=docker.io/library/node:20-alpine #NGINX_IMAGE=docker.io/library/nginx:1.25-alpine +# 前端构建依赖源;默认使用 npm 官方源,受限网络可设置为可信内网代理或公开镜像。 +#NPM_REGISTRY=https://registry.npmjs.org # ================================ # CA 宿主目录覆盖(可选;默认 /data/ocm/ca,写入需要 root) # 根目录只读的开发机(如 macOS)可指向用户可写路径,init-ca.sh 将免 root 运行; # gateway / asset-manager 容器内路径仍为 /data/ocm/ca,不受影响。 # ================================ -#OCM_CA_HOST_DIR=/home//.ocm/data/ocm/ca +#OCM_CA_HOST_DIR=${HOME}/.local/share/ocmanager/ca # ======================================================================== # 第六部分:服务版本(发布时更新) @@ -264,4 +343,5 @@ CLICKHOUSE_VERSION=23.8.8.20 FOOTSTONE_VERSION=v1.0.0 MSG_ETL_VERSION=v1.0.0 SYSDIAGNOSE_VERSION=v1.0.0 +REPOSITORY_WORKER_VERSION=v1.0.0 FRONTEND_VERSION=v1.0.0 diff --git a/docs/FAQ.md b/docs/FAQ.md index 1d94afbfb4daa5af4fb49305fe44bd1d8872f795..2041904140c74fcdb9be2c96108eef876321db60 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -11,7 +11,7 @@ - **控制平面**(`infra` + `manager` + `tms`)与**被管业务机**天然分离:Agent 跑在业务机上,控制台与数据通道跑在运维管控区。 - **是否独占物理机/虚拟机**:取决于规模与合规。小规模可与其他非冲突服务同机;**至少**保证 **CPU / 内存 / 磁盘 I/O / 网络** 不被邻域应用抢光,并为 **Docker 数据卷**、**日志目录**(如 `/data/ocm`、`/var/log/ocmanager`)预留空间。 -**软件依赖**(与 [QUICKSTART.md](QUICKSTART.md) 一致):Docker 20.10+(含 Compose v2)、一键部署时 **Go 1.24+**;本地开发另需 Node 18+;本机跑 `tms/file-svc` 时需 **gcc**(CGO)。 +**软件依赖**(与 [QUICKSTART.md](QUICKSTART.md) 一致):Docker 20.10+(含 Compose v2)、一键部署时 **Go 1.25+**;本地开发另需 Node 20+;本机跑 `tms/file-svc` 时需 **gcc**(CGO)。 **硬件量级(经验级、非 SLA)**:开发/POC 可自 **约 4 vCPU / 8 GiB RAM** 起;含 MySQL、Redis、Kafka、ZooKeeper、ClickHouse、manager 三进程、数据通道七服务时,**更稳妥**从 **8 vCPU / 16 GiB RAM** 起做功能验证。 diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 449683770415ead9a9c3f495877b2edc921d6e17..42b18e48cf5f3a311ee7c91de2cc04c4e6bcb7ab 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -11,19 +11,20 @@ | 依赖 | 版本 | 说明 | |------|------|------| | Docker | 20.10+ | 含 Compose v2(`docker compose ...`) | -| Go | 1.24+ | `deploy.sh` 内部会 `make build-bin-static` 在本机静态编译后端二进制,再 COPY 进镜像 | -| make / curl / tar | — | 系统通常自带;最小镜像里需要 `dnf install -y make tar` | +| Go | 1.25+ | `deploy.sh` 内部会 `make build-bin-static` 在本机静态编译后端二进制,再 COPY 进镜像 | +| make / curl / tar / openssl | — | 最小 OC9 需要 `dnf install -y make curl tar openssl`;`openssl` 用于生成部署 secret 和初始化 TMS CA | -> ⚠️ **镜像源提示**:所有镜像默认经 `mirror.ccs.tencentyun.com` 拉取,该地址**仅腾讯云内网可达**。 -> 非腾讯云环境(本地开发机 / 其他云 / 海外)请先在 `config/.env` 中切换镜像源,否则拉取会超时失败: +> **镜像源提示**:默认从 Docker Hub 拉取公开镜像。隔离网或使用内网 Registry 时,请在 `config/.env` 中覆盖镜像源: > > ```bash > OCM_IMAGE_MIRROR=docker.io +> APK_MIRROR=mirrors.cloud.tencent.com +> NPM_REGISTRY=https://registry.npmjs.org > ZOOKEEPER_REPO=bitnamilegacy/zookeeper # bitnami 官方目录已迁移,历史 tag 在 bitnamilegacy/ 下 > KAFKA_REPO=bitnamilegacy/kafka > GO_IMAGE=docker.io/library/golang:1.25-alpine > RUNTIME_IMAGE=docker.io/library/alpine:3.19 -> NODE_IMAGE=docker.io/library/node:18-alpine +> NODE_IMAGE=docker.io/library/node:20-alpine > NGINX_IMAGE=docker.io/library/nginx:1.25-alpine > ``` > @@ -127,7 +128,23 @@ bash scripts/deploy.sh up # 自动拉起 ocai-service + nginx 反代 完整说明见 **[OCAI-DEPLOY.md](OCAI-DEPLOY.md)**。 -若暂不部署 AI,保持 `ENABLE_OCAI_DEPLOY=false`(默认)即可;也可在 `Header.vue` 中隐藏 ``。 +若暂不部署 AI,保持 `ENABLE_OCAI_DEPLOY=false`(默认)即可。此时基础控制面、TMS 和 Repository 的确定性规则诊断仍正常工作,只有 OCAI 模型建议不可用;健康检查不会把未启用的 OCAI 计为失败。 + +### 可选组件与部署规模 + +`bash scripts/deploy.sh up` 默认只部署基础控制面(infra、TMS、manager),不要求 Repository Storage Node、OCAI 或 Agent 已经存在。附加能力按需要独立启用: + +| 运行规模 | 部署结果 | +| --- | --- | +| 0 Agent | 控制台、主机管理基础服务和 TMS 控制面可启动;分发目标列表为空 | +| 1 Agent | 可执行单目标命令和单目标 Repository Revision 分发 | +| 多个 Agent | 同一不可变 Revision 可选择一个或多个在线目标,分别记录结果 | +| 无 Repository Storage Node | 基础控制面正常;仓库页面可访问,但同步、上传和发布需要先注册在线仓库节点 | +| 无 OCAI | 规则诊断、日志摘要和其他组件正常;仅 AI 建议入口返回不可用状态 | + +基础控制面启动后可运行 `make -C manager repo-basic-acceptance`,该入口不要求 OCAI、Repository Node 或 Agent。需要执行包含 Core 与 Enhanced 的完整矩阵时使用 `make -C manager repo-acceptance-matrix`;最终恢复与发布总门禁仍使用 `make -C manager repo-acceptance`。 + +Repository Storage Node 使用独立 Compose 部署,Agent 使用独立安装包接入。任一可选组件离线时,控制面继续运行,并在对应页面显示离线或不可用;不会把它转换成基础部署失败。完整 Release Acceptance 仍要求本次要验收的增强功能具备实际前置条件。 --- @@ -143,17 +160,23 @@ bash scripts/deploy.sh up # 自动拉起 ocai-service + nginx 反代 # === 控制平面机器(deploy.sh up 已完成)=== export OCM_CA_KEY_PASSPHRASE='你的 CA 口令' -bash tms/deploy/scripts/init-ca.sh --resign-server # 缺 server.crt 时;勿用 --force +bash tms/deploy/scripts/init-ca.sh --resign-server # 缺 server.crt 或 SAN 需更新时;勿用 --force bash scripts/deploy.sh tms restart asset-manager gateway -# === Agent 仓打包(GATEWAY_ADDR 填 Agent 能连到的地址:8989)=== -GATEWAY_ADDR=127.0.0.1:8989 bash scripts/build-tar.sh # 同机验证示例 +# === Agent 仓打包(把示例域名替换为 Agent 能连到的地址)=== +GATEWAY_ADDR="management.example:8989" bash scripts/build-tar.sh # === 目标主机安装 === -sudo bash install.sh +export AGENT_PACKAGE_DIR="$PWD/ocm-agent-package" +export GATEWAY_ADDR="management.example:8989" +# 在 OCManager 源码根目录执行,覆盖并校验不同 Agent 模板中的默认地址 +bash scripts/configure-agent-package.sh "$AGENT_PACKAGE_DIR" "$GATEWAY_ADDR" +(cd "$AGENT_PACKAGE_DIR" && sudo bash install.sh) journalctl -u ocm-agent -n 30 --no-pager ``` +Repository 分发目录默认按 Agent 运行用户自动解析,不要求输入某台机器特有的 home。独立数据盘场景可为 systemd 服务设置 `OCM_REPOSITORY_MIRROR_ROOT=<分发镜像数据目录>`;该值必须是绝对路径。 + ### Agent 注册(默认需配置) 部署默认 **`auto_register: false`**:Agent 安装后若日志报 `agent not in whitelist`,说明尚未登记。 @@ -169,7 +192,7 @@ systemctl restart ocm-agent # 在 Agent 主机执行 **生产**——手动加白名单(uuid/ip/mac 与 Agent 日志 `fingerprint detected` 一致): ```bash -curl -sS -X POST "http://127.0.0.1:8080/api/v1/assets/add" \ +bash tms/deploy/scripts/gw-sign.sh -X POST "http://127.0.0.1:8080/api/v1/assets/add" \ -H 'Content-Type: application/json' -H 'X-User-Name: admin' \ -d '{"uuid":"","ip":"","mac":""}' ``` @@ -203,7 +226,7 @@ curl -sS -X POST "$API/api/v1/agents/status" -H 'Content-Type: application/json' | Docker | 20.10+ | 含 Compose v2(`docker compose ...`) | | Go | 1.24+ | 编译后端 | | Node.js | 18+ | Vite 6 要求(推荐用 `nvm use 18`) | -| Make / bash | — | Linux/macOS 自带;Windows 建议 WSL2 | +| Make / bash | — | OpenCloudOS 9 使用 `dnf install -y make bash` 安装 | **执行用户权限**:与方式一相同(当前用户能直接调用 `docker` + 能 `sudo` 用于偶发的系统操作)。 @@ -360,13 +383,15 @@ Vite 监听 `0.0.0.0:3070`,代理规则指向上一步后端的 127.0.0.1 端 | 现象 | 可能原因 / 处置 | |------|-----------------| -| `deploy.sh` 构建失败 | 1) `bash scripts/check-environment.sh` 自检;2) 确认 Docker ≥ 20.10 已启动;3) `go version` ≥ 1.24;4) `docker info` 当前用户能直接调用;5) `.env` 中 `SERVER_HOST` 与本机 IP 一致 | -| 镜像拉取超时(`i/o timeout` / `context deadline exceeded`) | 默认镜像源 `mirror.ccs.tencentyun.com` 仅腾讯云内网可达;非腾讯云环境按「前置依赖」中的镜像源提示切换 `OCM_IMAGE_MIRROR` 等变量 | +| `deploy.sh` 构建失败 | 1) `bash scripts/check-environment.sh` 自检;2) 确认 Docker ≥ 20.10 已启动;3) `go version` ≥ 1.25;4) `docker info` 当前用户能直接调用;5) `.env` 中 `SERVER_HOST` 与本机 IP 一致 | +| 镜像拉取超时(`i/o timeout` / `context deadline exceeded`) | 确认 Docker Hub 或配置的内网 Registry 可达;按「前置依赖」中的镜像源提示设置 `OCM_IMAGE_MIRROR`、`GO_IMAGE`、`RUNTIME_IMAGE`、`NODE_IMAGE` 和 `NGINX_IMAGE` | +| TMS 构建时报 `apk ... DNS lookup error` / `no such package` | Registry 可达不代表 Alpine 包源可达;在 `config/.env` 设置可解析的 `APK_MIRROR`。默认使用 `mirrors.cloud.tencent.com`,也可使用企业内网镜像、`mirrors.aliyun.com` 或官方 `dl-cdn.alpinelinux.org`;重新运行环境检查后再构建 | +| 前端构建时报 npm `ECONNRESET` / `ETIMEDOUT` | 在 `config/.env` 设置当前网络可达的 `NPM_REGISTRY`;默认是 `https://registry.npmjs.org`,隔离网应指向已同步依赖的可信内网代理。环境检查会访问 `/-/ping`,Dockerfile 使用锁文件并执行有限重试,不升级依赖版本 | | `permission denied while trying to connect to the Docker daemon socket` | 当前用户不在 docker 组:`sudo usermod -aG docker $(id -un) && newgrp docker`,或改用 root 执行 | | `make: go: Command not found` | 没装 Go 或不在 PATH:`source /etc/profile.d/go.sh` 或重新登录 shell;最小化方案见前置依赖 | | footstone 启动卡在 `setup plugin` | `trpc_go.yaml` 里的插件配置与实际引入的 import 不一致;对照 `configs/trpc_go.yaml.template` 检查 | | 前端 404 / 接口报 502 | `vite.config.mts` 里代理目标与后端实际端口不一致;或 footstone/sysdiagnose 未起 | -| MySQL 登录失败 | 容器第一次 init 未完成;`docker compose logs -f mysql` 观察 `ready for connections` | +| 首次部署迁移前短暂出现 MySQL `1045` | MySQL 第一次初始化会先启动 `@@port=0` 的临时 server,容器健康甚至 root 鉴权成功都不代表 initdb 已完成。当前 `deploy.sh` 最多等待 300 秒,确认鉴权成功且 `@@port > 0` 的最终 server 后再执行迁移;若仍超时,再核对 `config/.env` 的 `MYSQL_ROOT_PASSWORD` 和 MySQL 初始化日志 | | 页面可访问但登录失败 | 数据库 init SQL 未执行(检查 `docker compose down -v` 并重启)或手工改过 `login_users` 表 | | sysdiagnose / 命令助手连不上数据通道 | 步骤 3 中 `tms/api` 未监听 `8080`;或 `.env` 中 `TMS=` 与 `tms/api` 不一致;heartbeat/data-proxy 未加载 `REDIS_PWD` 导致 Redis 认证失败 | | Redis `connection refused`(数据通道) | `trpc_go.yaml` 中端口须为宿主映射端口(默认 `16379`),且 infra 已 `up -d` | diff --git a/docs/README.md b/docs/README.md index 4e4d47a82e0621af9ea19925ccb98bd1e4b6d88a..8a2fc11ef8d9c6a644d10b65387b0dcb4cc215e2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,5 +1,9 @@ # 文档索引 +私有化软件仓库对外提供方案设计、部署说明、测试验收、用户手册和结构化验收报告,分别说明 +系统架构、部署操作、复现方法、控制台日常操作和实际验收结果。具有代表性的历史测试保留在验收文档中并标明版本; +开发阶段计划、审计和实施过程记录仅作本地归档,不纳入代码 PR。 + ## 入门 | 文档 | 说明 | @@ -23,6 +27,11 @@ | [architecture/tms-channel.md](architecture/tms-channel.md) | **数据通道**:agent 接入侧 7 个服务的边界、契约、共享 infra 约定 | | [architecture/sysdiagnose-command-helper.md](architecture/sysdiagnose-command-helper.md) | 命令助手:双 task_id、数据通道/CK 回传、排障 | | [architecture/data-flows.md](architecture/data-flows.md) | 鉴权 / 命令下发 / 指标采集 时序图 | +| [私有化软件仓库方案设计.md](私有化软件仓库方案设计.md) | 私有化软件仓库总体方案、系统架构和关键技术设计 | +| [私有化软件仓库部署说明.md](私有化软件仓库部署说明.md) | 管理面、仓库节点、TMS、OCAI 的部署和恢复步骤 | +| [私有化软件仓库测试验收.md](私有化软件仓库测试验收.md) | Release Acceptance 复现、判定标准和 10 分钟现场演示 | +| [私有化软件仓库用户手册.md](私有化软件仓库用户手册.md) | 控制台使用说明:建仓、同步、上传、包管理、节点运维、分发与客户端配置 | +| [私有化软件仓库结构化验收报告.md](私有化软件仓库结构化验收报告.md) | 实际实现、数据库、接口、前端操作、截图和结构化验收结果 | ## 参考 diff --git a/docs/architecture/README.md b/docs/architecture/README.md index a1ca1139db849a2aa5fd643bf91de936b418ccb2..0d251a3916da3a01b7d5f87698ef22749df22d28 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -10,3 +10,4 @@ | [tms-channel.md](tms-channel.md) | **数据通道**:agent 接入侧 7 个 tRPC 服务的边界、契约、共享 infra 约定 | | [sysdiagnose-command-helper.md](sysdiagnose-command-helper.md) | 命令助手:双 task_id、与数据通道/CK 协作、排障 | | [data-flows.md](data-flows.md) | 鉴权 / 命令下发 / 指标采集 时序图 | +| [private-repository-plan.md](private-repository-plan.md) | 早期架构演进和实验记录;非当前部署或验收指南 | diff --git a/docs/architecture/frontend-app.md b/docs/architecture/frontend-app.md index 2ab71f90eb108353e6262b8f4e489003185dc2b9..a055cb6e729b8ed37ba733391fc0e74cfaea684a 100644 --- a/docs/architecture/frontend-app.md +++ b/docs/architecture/frontend-app.md @@ -11,7 +11,7 @@ | 模块 | 选型 | |------|------| | 框架 | **Vue 3**(Composition API) | -| 构建 | **Vite 6**(要求 Node.js 18+) | +| 构建 | **Vite 6**(项目依赖基线要求 Node.js 20+) | | UI 组件库 | **TDesign for Vue 3** | | 状态管理 | **Pinia** | | 路由 | **Vue Router 4** | @@ -87,7 +87,7 @@ src/ ## 设计约束 -- **Node.js 18+**:Vite 6 的下限,上游不再兼容 14/16。 +- **Node.js 20+**:与当前锁定依赖及前端容器保持一致。 - **不依赖内网域名**:任何内网地址都应作为可配置项;开发期用 `127.0.0.1`。 - **大依赖谨慎升级**:Vue / Vite / TDesign / TypeScript 这些 major 升级走独立 PR 讨论。 diff --git a/docs/architecture/private-repository-plan.md b/docs/architecture/private-repository-plan.md new file mode 100644 index 0000000000000000000000000000000000000000..b1356dbd27f7c3e77861bea28e98d9ca1c97b77b --- /dev/null +++ b/docs/architecture/private-repository-plan.md @@ -0,0 +1,1012 @@ +# OCManager 私有化软件仓库 V3.1 历史执行基线 + +> 本文仅保留早期 V3.1/V4.0 架构演进和旧拓扑实验,不是当前部署指南或发布状态。 +> 当前设计、操作和 152 项验收分别见[方案设计](../私有化软件仓库方案设计.md)、 +> [部署说明](../私有化软件仓库部署说明.md)、[测试验收](../私有化软件仓库测试验收.md) +> 和[结构化验收报告](../私有化软件仓库结构化验收报告.md)。旧 `31/31 PASS` 不能代替当前版本的完整验收。 + +## 1. 状态 + +- 架构状态:`FROZEN` +- 文档性质:V3.1/V4.0 阶段执行记录,不作为当前版本声明 +- 功能编码:独立 Repository Node、容量预检、日志治理、OCAI 产品入口和恢复链路已实现 +- 最终聚合门禁:AC-31 `Release Acceptance` +- V3.1 历史基线:AC-01 至 AC-31 `31/31 PASS` +- V4.0 历史完整验收:Build130、Build143 均获得 31/31 PASS,最近一次为 Build143 +- 最近干净部署证据:Build154 基础拓扑预检 42/42、健康检查 17/17 +- 管理面:JWT 鉴权 +- Repository 数据面:隔离网内免认证、严格只读 + +## 2. 冻结架构 + +```mermaid +flowchart LR + UI[Vue 3 Console] --> N[Nginx] + N -->|JWT management API| API[footstone Repository API] + API --> DB[(MySQL)] + API -->|mTLS task/control| W[Repository Node Worker] + W -->|rw| FS[(Repository Node bind mount)] + P[Repository Node Nginx] -->|GET/HEAD, ro| FS + DNF[OC9 DNF Client] -->|HTTP| P + API --> TMS[Existing TMS] + TMS --> AG[Existing Agent Nodes] + AG -->|pull immutable revision| RN[Target Repository Nodes] +``` + +Repository API 继续落在 footstone 现有 presentation、application、domain、 +infrastructure 分层内。worker 使用同一 Go module,但采用两个薄入口,禁止把 +Repository 专用生命周期塞入 footstone 主入口: + +```text +manager/backend/cmd/footstone/ +manager/backend/cmd/repository-worker/ +``` + +## 3. Repository 模型 + +核心模型包括 Repository、Revision、Package、Task、Mutation Lock、Validation、 +Distribution 和 GPG key metadata。Repository 按以下结构在 API 与前端中组织: + +```text +System/Product + -> Version + -> BASELINE / UPDATE / PRIVATE Repositories +``` + +MySQL Repository 模型是唯一运行时事实来源。批量 JSON 必须通过 +`BatchApplyRepositories API -> Application Service -> DB`,禁止脚本直接写数据库或 +Repository 目录。 + +Revision 发布后不可修改。Validation 和 Distribution 在创建时必须绑定 +`repository_id + revision_id + manifest_sha256`,不得在执行过程中重新解析 +`current` 或 `latest`。 + +## 4. Storage Boundary + +默认使用宿主机 bind mount: + +```yaml +${REPOSITORY_DATA_HOST_DIR:-/opt/ocmanager/repository-data}:/var/lib/ocmanager/repository-data +``` + +API 与 worker 挂载 `rw`,Nginx 挂载 `ro`;三个容器内部路径必须完全一致。 +宿主目录用于备份、Evidence 导出和现场复现。 + +```text +repository-data/ +├── staging/ +├── quarantine/ +├── revisions-private/ +├── task-logs/ +├── keys/ +├── trash/ +└── public// + ├── revisions// + └── current -> revisions/ +``` + +`current` 必须是相对 symlink。worker 在同一文件系统创建临时 link,再使用原子 +rename 替换。Nginx `/repositories/` 只允许 GET/HEAD,其他方法返回 405, +`autoindex off`,上传只能经过 JWT management API。 + +## 5. Storage Capacity POC + +Phase 0 使用 disposable host root,不写正式 `/opt` 目录。逐阶段记录字节数、文件数、 +剩余空间和 inode: + +1. 空目录基线。 +2. 文件进入 staging。 +3. 合法包形成 revision,损坏包进入 quarantine。 +4. 生成 repodata 并切换 current。 +5. 发布第二个 revision,保留 current 与 previous。 +6. 删除内容进入 trash。 +7. 清理 staging、quarantine 和 trash。 +8. 验证 current symlink 不复制数据。 +9. 模拟空间不足,确认 publish 前失败且 active revision 不变。 +10. restart/redeploy 后确认没有孤儿 staging 或重复 revision。 + +容量 preflight 公式: + +```text +required_free = +active_revision_size ++ max_import_expanded_size ++ projected_new_revision_size ++ metadata_reserve ++ safety_margin +``` + +默认 `safety_margin=20%`,硬阈值 90%。达到硬阈值时拒绝新 mutation,但不影响读取。 +空间理论值与实际值差异超过 5%、清理后存在未知占用、空间不足仍发布,均判定失败。 + +## 6. GPG Boundary + +`repository_gpg_keys` 只保存: + +```text +id +repository_id +key_id +fingerprint +public_key_path +created_at +``` + +公钥文件位于 `keys///public.key`。数据库不保存 key body, +产品不保存或管理私钥。首版必须支持 `DISABLED` 与 `VERIFY_PACKAGE`;正确签名包通过 +校验,未签名、错误签名或未授权 key 的包进入 quarantine,active revision 保持不变。 + +## 7. Task And Lock + +`sync`、`upload commit`、`delete package`、`rebuild metadata`、`delete repository` 和 +`publish` 都必须取得 Repository Mutation Lock。锁包含 repository ID、task owner、 +lease expiry、heartbeat 和 fencing token。 + +同一 Repository mutation 串行,不同 Repository 可并发。Remote Sync 采用完整 snapshot +语义,只允许 `SUCCESS/FAILED`;任一关键失败都不发布。只有 tar.gz Upload 可以在同时 +存在 accepted 与 rejected 包时返回 `PARTIAL_SUCCESS`。 + +## 8. Multi-node Distribution + +复用当前 OCManager 的 TMS/Agent 链路,不修改外部 Agent 仓库。节点执行: + +```text +download exact revision to staging +-> verify manifest SHA-256 +-> verify every file SHA-256 +-> move to revisions/ +-> atomic relative symlink switch +-> return structured result +``` + +Phase 0 按三级验证: + +1. L0:Agent 执行 `echo hello`,验证目标、stdout、exit code 和状态查询。 +2. L1:下载小文件并验证 SHA-256。 +3. L2:下载最小 Repository snapshot,验证 manifest、原子切换和本地 DNF metadata。 +4. L2 至少验证一个 Agent;环境提供多个 Agent 时,再使用同一任务验证多目标分发。 + +Phase 9 已把这条链路从 POC 提升为产品能力:Distribution 创建时绑定 +`repository_id + revision_id + manifest_sha256 + repomd_sha256`,TMS 只下发轻量执行脚本, +Agent 从精确 Revision URL 拉取 manifest 和清单内文件。manifest schema v2 为每个 RPM 记录相对路径和 +SHA-256,因此不需要额外复制一份仓库压缩包。目标节点必须完成全文件摘要、repomd 摘要和 DNF metadata +校验后,才会把相对 `current` 链接原子切换到该 Revision。 + +## 9. DNF Trust Model + +每个用例使用独立干净 OC9 容器,并使用产品 Generator 生成的 `.repo`: + +```text +--disablerepo='*' +--enablerepo='' +``` + +- Case A:只启用 Sync Repo,安装 `ocm-sync-only`。 +- Case B:只启用 Private Repo,安装无外部依赖的 `ocm-private-only`。 +- Case C:只启用 Sync + Private,安装依赖 `ocm-sync-dependency` 的 + `ocm-private-app`。 + +Evidence 必须包含 repo ID、baseurl、NEVRA、provider/source repo、DNF exit code、 +`rpm -q` 结果和耗时。 + +## 10. Frontend Boundary + +前端技术边界冻结为 Vue 3、Vue Router、Pinia、TDesign Vue Next 和现有 +Axios/fetchApi。禁止新增 UI framework、CSS framework、状态管理库或重复 HTTP client。 + +## 11. Acceptance + +评委只展示 6 个 Core 分类和 3 个 Enhanced 分类: + +```text +Core: Repository Management, Package Ingestion, Repository Publishing, + DNF Validation, Automation, Security +Enhanced: Distribution, Diagnosis, Repo Generator +``` + +内部保留 AC-01 至 AC-30,以 AC-31 `Release Acceptance` 聚合。Evidence 使用 9 个分类 +Bundle 加 `summary.json`,避免维护 31 套重复报告。任何内部检查失败,所属分类即失败, +项目不得标记 `FINAL READY`。 + +## 12. Recovery Acceptance + +最终验收包含: + +1. Restart API、worker、Nginx、TMS。 +2. 删除并重建应用容器,保留 bind data 和数据库。 +3. 仅在 disposable 环境销毁应用与测试数据库,从 MySQL backup、Repository backup 和 + manifest 恢复。 +4. 恢复后重新执行完整 acceptance。 + +恢复前后 Repository ID、active revision、manifest digest、inventory、GPG metadata 和 +Validation Report 必须一致。 + +## 13. Phase 0 Gate + +只有以下 POC 全部通过才能开始功能编码: + +- OC9 tools/GPG +- bind mount、路径与权限 +- atomic symlink、Nginx restart +- storage capacity lifecycle +- TMS L0/L1/L2 单目标或多目标 +- isolated protobuf generation +- restart/redeploy recovery +- Phase 前后无未知 worktree 修改 + +## 14. Phase 0 Execution Record + +执行基线:仓库提交 `35102d6300bfc2c6ac20d2cb27f92ac08b2c5e4c`。 + +非 OpenCloudOS 环境的结果不作为 OC9 验收证据。管理节点已确认是 OpenCloudOS 9.6 +x86_64、XFS、SELinux Enforcing,并已安装 Docker 28.0.1、Compose 2.32.1、Go 1.26.3、 +Git 2.43.7、createrepo_c 0.20.1、RPM/rpmkeys/rpm-sign 4.18.2、GnuPG 2.4.3、 +Nginx 1.29.8、protoc 24.2 和 OpenSSL 3.0.12。官方 OC9.6 container image 的 sidecar +SHA-256 校验及本地加载均通过。 + +### 14.1 Gate Status + +| Gate | Status | Evidence summary | +| --- | --- | --- | +| P0-0 Worktree and environment | `PASS` | OC9 工具链、Docker/Compose 和独立 Agent 节点已就绪;本地主工作区无未知变化。 | +| P0-1 OC9 tools and GPG | `PASS` | 授权签名通过;未授权、未签名和损坏 RPM 均拒绝。 | +| P0-2 Bind mount and atomic publish | `PASS` | API/worker `rw`、Nginx `ro` 使用相同容器路径;GET/HEAD 200、POST 405;swap、restart、recreate、redeploy 后读取均为 200。 | +| P0-3 Storage capacity lifecycle | `PASS` | staging、quarantine、双 revision、trash、清理、symlink 零复制和空间不足保护均通过。 | +| P0-4 TMS distribution ladder | `PASS` | 单节点 L0/L1/L2 通过;同一 TMS task 面向两个 Agent 的 immutable-revision L2 也通过。 | +| P0-5 Protobuf generation | `PASS` | 临时副本中只生成 Repository proto,4 个相关生成物,0 个无关变化。 | +| P0-6 Recovery smoke | `PASS` | HTTP revision 在 restart/recreate/redeploy 后保持;TMS 任务运行中重启 task-manager 后成功回执;VM 重启后控制面、Agent ID 和 revision 均恢复。 | +| P0-7 Evidence and gate | `PASS` | 单目标及多目标任务、环境、恢复和 worktree 证据已导出,凭据排除检查与 SHA-256 清单校验通过。 | + +单节点 TMS 阶梯的不可变发布对象为 +`rev-l2-b304f05693a74218`,snapshot SHA-256 为 +`b304f05693a7421864e9ff4fbd35302fcb5b93093bba365ca4bf71aff73edba1`,manifest +SHA-256 为 +`7cf23ce85dc8afdf3f3121ed4142561fe15e7c894df7746bf8c392756910cd44`。三个任务均返回 +target `SUCCESS`,并分别验证 `L0_HELLO`、`L1_SHA256_OK` 和 +`L2_DNF_METADATA_OK`。官方 `ocm-agent` 源码只读克隆固定在提交 +`3805983e4fbc62cdacde28cd0747022f94795c69`,未修改外部仓库。 + +多目标 L2 历史验收使用同一个任务选择两个独立 Agent。两个 target 均为 +`TARGET_ACKED_SUCCESS`,发布相同 immutable revision,且 snapshot、manifest 和两端 +`repomd.xml` 的 SHA-256 一致。第二个 Agent 重启后从既有证书恢复同一 Agent ID,revision 与相对 +`current` 链接保持不变。这是多目标能力的增强证据,不代表普通部署必须固定准备两个 Agent。 + +### 14.2 Scope Assessment + +| Finding | Classification | Evidence | Affected feature | Minimum required fix | Regression scope | +| --- | --- | --- | --- | --- | --- | +| `init-ca.sh` 生成的 CA 缺少 `cRLSign`,asset-manager 无法生成 CRL,Gateway 拒绝 Agent mTLS | `BLOCKER` | 原脚本生成证书与 asset-manager/Gateway 运行日志;POC 加入 `keyCertSign,cRLSign` 后注册和 L0/L1/L2 通过 | Repository multi-node distribution | 后续仅修正当前仓库 `tms/deploy/scripts/init-ca.sh` 的 CA 扩展并补证书回归测试 | TMS 首次部署、Agent 注册、证书撤销 | +| `/healthz` 使用非法空 MD5 探测 file-svc,整体返回 503,但上传和 RUN_ARTIFACT 正常 | `NON-BLOCKER` | health JSON 显示其他三项正常;file upload smoke 与三个任务均通过 | 不阻塞 Repository 分发 | 本次不修改;Repository preflight 使用直接能力探针 | 无本次回归范围 | +| 任务级 `deploy_status` 返回 `TASK_SUCCESS`,与文档的 `SUCCESS` 不一致 | `NON-BLOCKER` | query/detail JSON;target 级仍正确返回 `SUCCESS` | 自动化状态解析 | 客户端暂兼容两种任务级枚举,最终以 target 明细为准;本次不修主干 | 无本次回归范围 | +| 成功的 `exit_code=0` 因 JSON `omitempty` 未显式出现 | `NON-BLOCKER` | target detail 中 `SUCCESS`、输出存在但无 `exit_code` 字段 | Evidence 展示 | POC 记录 `exit_code_present=false` 并按成功协议归一为 0;Feature Phase 再决定是否最小修复 DTO | 无本次回归范围 | +| 默认 Compose 镜像源在当前网络不可解析 | `NON-BLOCKER-ENV` | 镜像拉取日志;固定提交源码构建的七个服务可运行 | Phase 0 环境复现 | POC 使用本地构建,不修改正式 Compose | 无本次回归范围 | + +### 14.3 Current Decision + +Phase 0 当前为 `PASS / READY FOR PHASE 1`。单目标链路已通过,同一 Distribution Task 的多目标增强用例也已完成 +immutable revision 拉取、manifest 与文件校验、原子切换及本地 DNF metadata 验证。主工作区 +继续固定为当前本地主 Git 工作树的提交 +`35102d6300bfc2c6ac20d2cb27f92ac08b2c5e4c`;仓库节点未克隆主仓库,远端只保存 +disposable Agent 与分发结果,避免形成并行开发副本。 + +`init-ca.sh` 的 `cRLSign` 缺失仍是 Multi-node Distribution 正式实现前的阻断项。Phase 0 +已用完全隔离的证书 POC 证明最小修复有效;进入 Feature Phase 后只允许按 14.2 的范围修复, +不得借此扩大 TMS 主干改动。 + +详细证据位于本地忽略目录 `.run/repository-poc/evidence/management-node/` 与 +`.run/repository-poc/evidence/repository-node/`。证据导出明确排除了 CA 私钥、Agent 私钥、Redis +密码与远端登录凭据。 + +## 15. Phase 1 Execution Record + +Phase 1 只实现 Repository 定义管理,不包含 worker、RPM 内容灌入、发布、前端页面或正式 +Distribution。数据库仍是 Repository 运行时唯一 source of truth;Batch Config 后续只能调用 +本阶段提供的 Batch Repository API,不得直接写表或创建目录。 + +### 15.1 Implemented Slice + +- 新增独立 `repository.proto`,提供 Create、Update、Describe、List、Delete 与原子 + Batch Apply 六个 RPC,并生成独立 Swagger 和 mock。 +- Repository 模型包含项目、System/Product、Version、架构、类型、上游、include/exclude、 + upload policy、GPG policy、状态及 immutable active revision 引用。 +- `storage_location` 与 `publish_key` 由服务端生成;API 不接受客户端指定存储路径。 +- 复用现有 project/JWT 上下文进行全局管理员或项目创建者授权;跨项目更新被拒绝。 +- Batch Apply 上限 100 项,创建与更新共享单个 MySQL 事务,任一项失败则全批回滚。 +- 删除必须显式确认;存在 active revision 时拒绝删除,内容清理语义留给后续 mutation task。 +- 新增 MySQL 幂等迁移,并同时接入 fresh Compose 初始化与现有 `deploy.sh` 增量升级入口。 + +### 15.2 Verification + +| Check | Result | Evidence | +| --- | --- | --- | +| Repository unit tests | `PASS` | PO JSON、MySQL 1062、规格/筛选校验、Batch 事务、Proto 双向映射全部通过。 | +| MySQL migration | `PASS` | OC9 上 MySQL 8.0.45 连续重复执行迁移,表、排序规则和全部索引保持一致。 | +| MySQL integration | `PASS` | CRUD、项目权限、幂等 Update、混合 Batch、失败全回滚、删除确认与 active revision 保护通过,测试数据清理为 0 行。 | +| Protocol regeneration | `PASS` | 专用 `make repository` 重生成后 4 个生成物 SHA-256 不变,无无关 proto/Swagger 变化。 | +| Footstone build | `PASS` | `go build ./cmd/footstone` 在 OC9 生成有效 x86-64 ELF。 | +| Deploy/Compose static validation | `PASS` | 两个 shell 脚本 `bash -n` 通过,Infra Compose 解析成功且包含迁移只读挂载。 | +| Worktree scope | `PASS` | 未修改 frontend、TMS、Agent、worker 或无关 generated files;未执行 commit/push。 | + +完整 `go test ./services/footstone/... -run '^$'` 仍会命中固定基线已有的两项失败: +`instancerepo` 测试 helper 重名导致测试包编译失败,以及 `infrastructure/lock` 测试初始化 +依赖未提供的外部锁服务。两项在未叠加 Repository 改动的 +`35102d6300bfc2c6ac20d2cb27f92ac08b2c5e4c` 副本中可同样复现,均为 +`NON-BLOCKER`,本阶段不修改。Repository 影响范围测试与 footstone 产品二进制构建均通过。 + +### 15.3 Gate Decision + +Phase 1 当前为 `PASS / READY FOR PHASE 2`。这只表示 Repository 定义管理的后端基础已就绪; +AC-01 至 AC-31 的最终状态仍需后续前端、任务、内容、发布、安全、E2E、诊断、分发和 +Release Acceptance 完成后才能标记 `PASS`。Phase 2 应只实现 Task、Lease 与 Repository +Mutation Lock,不得提前混入 RPM ingestion 或 worker 内容处理。 + +Phase 1 Evidence 位于本地忽略目录 `.run/repository-poc/evidence/phase1/`,包含环境、 +迁移与 schema、单元测试、MySQL 集成、协议复现、二进制构建和 Compose 静态验证日志。 + +## 16. Phase 2 Execution Record + +Phase 2 只建立 Repository 异步任务控制面,不执行 RPM 下载、校验、repodata 或发布。后续 +Sync、Upload、Delete Package、Rebuild 和 Publish 必须调用本阶段内部 `Enqueue`,管理 API +不开放任意任务类型和 payload 注入。 + +### 16.1 Task State Machine + +```text +PENDING -> RUNNING -> SUCCESS | FAILED | PARTIAL_SUCCESS +PENDING -> CANCELED +RUNNING -> CANCEL_REQUESTED -> CANCELED +RUNNING -- lease expired and attempts remain --> PENDING +RUNNING -- lease expired and attempts exhausted --> FAILED +FAILED | CANCELED -- user retry --> new linked PENDING task +``` + +`PARTIAL_SUCCESS` 只允许 Upload;Sync 等 snapshot 任务只能 SUCCESS 或 FAILED。用户 Retry +创建新 task ID,并保存 `root_task_id` 与 `retry_of_task_id`,不覆盖原任务历史。 + +### 16.2 Lease And Fencing + +- `repository_tasks` 持久化状态、attempt、worker、lease、heartbeat、progress、错误摘要与 + immutable `revision_id`。 +- `repository_mutation_locks` 每个 Repository 保留一行;释放后不删行,下一任 owner 的 + fencing token 单调递增。 +- heartbeat 和 complete 必须同时匹配 task ID、worker ID、未过期 lease 与 fencing token。 +- 旧 worker 即使继续运行,也不能用旧 token 完成 mutation 或释放新 owner 的锁。 +- 同一 Repository 的 mutation 串行;不同 Repository 可并发。 +- Validation 与 Distribution 必须绑定 immutable revision,作为只读任务不取得中央 mutation + lock,可与同仓 mutation 并行。 +- 恢复逻辑完全基于 MySQL;服务对象重建后可重新入队过期任务,不依赖进程内状态。 + +### 16.3 Management API + +Repository Service 新增 Describe Task、List Tasks、Cancel Task 与 Retry Task 四个 RPC。 +响应包含 attempt、lease、heartbeat、worker、fencing token、progress、错误摘要和时间字段, +但不返回内部 payload。授权继续使用现有 JWT/project 模型。 + +### 16.4 Verification + +| Check | Result | Evidence | +| --- | --- | --- | +| Task/lock migration | `PASS` | MySQL 8.0.45 连续执行两次,两个 InnoDB 表及全部复合索引一致。 | +| Same repository serialization | `PASS` | 两个并发 worker 面向同仓 mutation,仅 1 个成功 claim。 | +| Cross repository concurrency | `PASS` | 两个并发 worker 面向不同 Repository,2 个均成功 claim。 | +| Immutable read-only concurrency | `PASS` | 同仓 mutation 持锁时 Validation 成功 claim,token 为 0。 | +| Heartbeat and ownership | `PASS` | 正确 token 续租;错误 worker/token 被拒绝。 | +| Fencing takeover | `PASS` | 过期 owner token 3,新 owner token 4;旧 owner completion 被拒绝。 | +| Lease recovery | `PASS` | attempt 未耗尽回到 PENDING;第 2 次过期转 FAILED;服务对象重建后仍恢复。 | +| Cancel/retry transitions | `PASS` | pending cancel、running cancel request、worker canceled completion 和 linked retry 通过。 | +| Completion semantics | `PASS` | Sync PARTIAL_SUCCESS 被拒绝,Upload PARTIAL_SUCCESS 通过。 | +| Unit/API mapping | `PASS` | 输入、状态、ownership、Proto 枚举和 timestamp 映射通过。 | +| Protocol/build/deploy | `PASS` | 生成物哈希复现、footstone 构建、shell 与 Compose 解析通过。 | +| Cleanup/worktree | `PASS` | disposable 数据清理为 0 行,无未知主工作区修改。 | + +### 16.5 Gate Decision + +Phase 2 当前为 `PASS / READY FOR PHASE 3`。Async Task 最终 AC 仍为 `IN_PROGRESS`,只有后续 +真实 Upload/Sync handler、worker 生命周期、前端状态展示、retry/cancel E2E 和恢复验收全部 +通过后才能标记 `PASS`。Phase 3 应实现 Private RPM/tar.gz ingestion、quarantine 与 NEVRA +管理,并复用本阶段 mutation lock;不得另建第二套任务或锁通路。 + +Phase 2 Evidence 位于 `.run/repository-poc/evidence/phase2/`。 + +## 17. Phase 3 Execution Record + +Phase 3 只实现私有包内容灌入,不生成 repodata、不切换 HTTP active revision,也不提前实现 +Remote Sync。管理面继续复用现有 JWT/project 授权;上传采用 tRPC chunk RPC,避免建立绕过 +footstone 的第二条写入通路。上传完成与 UPLOAD task 入队在同一 MySQL 事务中提交。 + +### 17.1 Ingestion And Storage + +- 支持单个 `.rpm` 与含多个 RPM 的 `.tar.gz`,校验总大小、chunk offset、chunk 大小及 + 完整文件 SHA-256。 +- tar 解包拒绝绝对路径、路径穿越、symlink、非普通文件、非 RPM、控制字符/超长文件名、 + 文件数超限和展开大小超限。 +- worker 在 OpenCloudOS 9 runtime 中执行 `rpm -K --nosignature` 与 `rpm -qp`,提取并校验 + NEVRA;`noarch` 可进入 x86_64/aarch64 仓,其余包必须与仓库架构一致。 +- 同 NEVRA、同 SHA-256 视为幂等重复;同 NEVRA、不同 SHA-256 进入 quarantine。 +- 单 RPM 失败时 task 为 `FAILED`;tar 中部分包有效时 task 为 `PARTIAL_SUCCESS`,有效包进入 + 新 revision,被拒绝包进入 quarantine,二者明细均可查询。 +- 私有 revision 写入 `revisions-private//`,每次 upload 或按精确 + NEVRA 删除都创建 immutable revision。task 在 fenced completion 前绑定该 revision ID。 +- revision 构建按“父 revision 全量复制退化 + 新包”执行保守空间检查,默认额外保留 1 GiB, + 并继续执行文件系统 90% 硬阈值保护。 +- Repository 在私有 revision 成功入库后进入 `DRAFT_READY`;Phase 4 完成 repodata 后才允许 + 进入正式 published/active 状态。删除 Repository 前会检查 upload、revision 和 quarantine, + 不允许绕过内容确认直接删除。 + +### 17.2 Worker Boundary + +独立入口位于 `manager/backend/cmd/repository-worker/`,业务实现位于 +`services/footstone/repositoryworker/`。footstone 主入口不包含 worker mode 分支。API 与 worker +使用同一 Repository bind root 和 MySQL task/lock 模型;worker 镜像使用 OC9 runtime,只有该 +镜像安装 `rpm` 工具。 + +`VERIFY_PACKAGE` 在 Phase 10 GPG verifier 完成前采用 fail-closed:启用该策略的仓库会返回 +`GPG_VERIFICATION_NOT_READY`,不会把未验证包加入 revision。数据库和 bind root 均不保存私钥。 + +### 17.3 Verification + +| Check | Result | Evidence | +| --- | --- | --- | +| Chunk upload and checksum | `PASS` | Begin/Append/Complete 使用固定 offset 和 SHA-256;checksum 不符不创建 task。 | +| Single RPM ingestion | `PASS` | 真实 OC9 RPM 进入 READY immutable revision,NEVRA、大小和 SHA-256 一致。 | +| Tar partial success | `PASS` | 1 个合法 RPM 接受、1 个损坏 RPM quarantine,task 为 PARTIAL_SUCCESS。 | +| Dangerous archive | `PASS` | `../escape.rpm` 整包拒绝,task FAILED,latest private revision 不变。 | +| Architecture filtering | `PASS` | x86_64 RPM 上传到 aarch64 仓被拒绝并以 WRONG_ARCHITECTURE quarantine。 | +| NEVRA delete | `PASS` | 精确 NEVRA 删除创建新 immutable revision,父 revision 保持不变。 | +| Repository/task state | `PASS` | Repository 为 DRAFT_READY,成功 task 记录实际 revision ID。 | +| Worker runtime | `PASS` | OC9 9.6 容器内 RPM 4.18.2,外部 worker task SUCCESS。 | +| Capacity guard | `PASS` | 父 revision 复制退化、新包及默认 1 GiB reserve 在落盘前检查。 | +| Protocol regeneration | `PASS` | 既有生成流程连续两次输出相同 4 个 SHA-256,无无关生成物变化。 | +| Unit/build/vet | `PASS` | Repository 影响范围 tests、presentation compile-only 与 go vet 通过。 | +| Compose | `PASS` | footstone/worker 使用相同容器路径和 rw bind mount,Compose 解析通过。 | + +### 17.4 Gate Decision + +Phase 3 为 `PASS / READY FOR PHASE 4`。Package Ingestion 最终 AC 仍为 `IN_PROGRESS`:前端上传 +体验、GPG 正负向闭环、发布后 DNF 隔离验证及最终 recovery/acceptance 尚未完成。Phase 4 应只 +增加 Remote Sync、snapshot 失败语义、标准 repodata 与 atomic publish 基础,不重复实现上传、 +任务、锁或 storage root。 + +Phase 3 Evidence 位于 `.run/repository-poc/evidence/phase3/`。 + +## 18. Phase 4 Execution Record + +Phase 4 在既有 task/lease/mutation-lock 与私有 revision 基础上增加 Remote Sync、标准 repodata、 +immutable public revision 和 atomic active switch。Upload、Delete Package 与 Sync 共用同一 +publisher;同步采用全量 snapshot 语义,只允许 `SUCCESS` 或 `FAILED`,任一下载、RPM、架构、 +大小、metadata 或 publish 失败都不改变 active revision。 + +### 18.1 Sync And Publication + +- `SyncRepository` 管理 API 在入队时固化 upstream URL、目标架构及 include/exclude,retry 复用 + 同一 payload,避免 Repository 编辑导致任务输入漂移。 +- worker 使用任务专属 DNF 配置和 cache,执行 `dnf reposync --norepopath --download-metadata`; + `--forcearch` 固定目标架构,只拉取目标架构与 `noarch`。 +- 下载结果逐个执行 SHA-256、RPM header/NEVRA 与架构校验;include 为空表示全选,exclude 始终 + 优先。空结果、越限、坏 RPM 或同 NEVRA 不同摘要均整次失败,不发布部分 snapshot。 +- `createrepo_c --checksum sha256 --revision ` 在 publish staging 中生成标准 + repodata;`manifest.json` 记录包清单、repomd 摘要及全部 repodata 文件摘要。 +- public revision 先在同文件系统 staging 完成,随后 rename 到 + `public//revisions/`;`current` 使用相对 symlink 和同目录 rename 原子替换。 +- `manifest_sha256` 保持 Phase 3 包清单语义;新增 `repomd_sha256` 和 + `snapshot_manifest_sha256`,为 immutable distribution 提供明确校验对象。 + +### 18.2 Fencing And HTTP Boundary + +- 最终发布使用短事务锁定 task 行和 mutation-lock 行,再校验 worker ID、fencing token、task/lock + lease 及 cancel 状态;下载和 metadata 生成不占用该事务。 +- link swap、revision/package 持久化、Repository active revision 与 task revision 绑定属于同一 + fenced commit。DB commit 失败时补偿恢复旧 link,并把未提交 public revision 移入 trash。 +- publication commit 获胜后 task 已绑定 immutable revision,后到的 Cancel 返回不可取消;Cancel + 先获得任务锁时 publisher 不会进入 callback。 +- Nginx 只挂载宿主 bind root 的 `public/` 子树且为 `ro`;`/repositories/` 只允许 GET/HEAD, + POST/PUT/PATCH/DELETE 返回 403,`autoindex off`。上传仍只能走 JWT 管理 API。 +- Repository worker 启动时 fail-fast 检查 `rpm`、`dnf` 和 `createrepo_c`;正式 Compose 的 worker + 使用 OC9 runtime 并安装 `dnf-plugins-core`、`createrepo_c` 与 RPM 工具。 + +### 18.3 Verification + +| Check | Result | Evidence | +| --- | --- | --- | +| Remote Sync | `PASS` | disposable HTTP upstream + 真实 `dnf reposync`,同步 noarch/x86_64 fixture。 | +| Include/exclude | `PASS` | `ocm-phase3-*` include 且排除 `ocm-phase3-two`,最终只发布 2 个预期 NEVRA。 | +| Snapshot failure | `PASS` | 不可达 upstream 返回 `SYNC_DOWNLOAD_FAILED`,active revision ID 保持不变。 | +| Standard repodata | `PASS` | `createrepo_c 0.20.1` 生成 repomd,隔离 `dnf makecache` 通过。 | +| Immutable revisions | `PASS` | 连续两次同步产生不同 revision,切换后旧 repomd SHA-256 不变。 | +| Atomic current | `PASS` | `current -> revisions/` 为相对链接,restart/recreate 后 HTTP 仍为 200。 | +| Fencing/cancel | `PASS` | stale token、过期 lease 与 CANCEL_REQUESTED 均无法进入 mutation callback;不同仓可并发。 | +| Upload/Delete publication | `PASS` | 单 RPM、tar PARTIAL_SUCCESS 与 NEVRA Delete 均生成 PUBLISHED revision/repodata。 | +| HTTP read-only | `PASS` | GET/HEAD 200,写方法与目录索引 403,容器内写入只读挂载失败。 | +| Worker image | `PASS` | OC9 镜像内 RPM 4.18.2、reposync 可用、createrepo_c 0.20.1。 | +| Migration | `PASS` | publication 两个摘要字段从旧表结构升级,连续执行两次成功。 | +| Proto/build/vet/Compose | `PASS` | Repository 独立 proto 生成;定向 unit/build/vet 与 `compose config` 通过。 | + +### 18.4 Gate Decision + +Phase 4 为 `PASS / READY FOR PHASE 5`,不是 `FINAL READY`。Phase 5 应实现动态 `.repo` 生成与正式 +发布 URL 产品接口;Phase 6 之后仍需前端、三类隔离 DNF install、诊断、单目标/多目标分发、GPG 闭环、 +总验收与完整恢复。因此 AC-31 `Release Acceptance` 继续为 `IN_PROGRESS`。 + +Phase 4 Evidence 位于 `.run/repository-poc/evidence/phase4/`。 + +## 19. Phase 5 Execution Record + +Phase 5 增加正式发布 URL 配置和动态 `.repo` 产品接口,不修改前端。管理 API 继续复用 footstone +RepositoryService 与项目鉴权;数据面继续由 Nginx 的只读 `/repositories/` 提供。 + +### 19.1 Generator Contract + +- 新增 `GenerateRepositoryConfig(repository_id, immutable_revision)`。响应同时返回 `repo_id`、 + `file_name`、`content_type`、完整 `.repo` 文本、实际 `baseurl`、immutable `revision_url`、 + active revision ID、enabled 和 GPG 字段,前端可直接实现 Copy 与 Blob Download,无需第二个下载接口。 +- 普通模式的 `baseurl` 使用 `current/`;Validation 设置 `immutable_revision=true` 后直接使用 + `revisions//`,测试期间即使中央仓发布新 revision 也不会漂移。 +- Generator 只接受 DB 状态为 PUBLISHED 的 active revision,并在输出前核验磁盘上的 `current` 是 + 相对 symlink、严格指向该 revision,且 `repodata/repomd.xml` 为非空可读普通文件。 +- `repo_id` 与文件名由不可变 `publish_key` 生成;Repository 展示字段拒绝控制字符,响应不暴露 + storage location、宿主机路径、容器路径或鉴权信息。 + +生成内容的基础形式为: + +```ini +[ocmanager-] +name=OCManager - - () +baseurl=//current/ +enabled=<0|1> +gpgcheck=<0|1> +repo_gpgcheck=0 +metadata_expire=300 +skip_if_unavailable=0 +``` + +### 19.2 Public URL And GPG Boundary + +- `REPOSITORY_PUBLIC_BASE_URL` 是唯一正式 URL source of truth,必须为无凭据、query、fragment 和 + 控制字符的绝对 HTTP(S) URL;不从请求 Host 动态推断。 +- Compose 不静默回退 localhost。`deploy.sh up` 在值为空时按 `CONNECT_HOST`、`SERVER_HOST`、 + `FRONTEND_PORT` 推导;地址仍为占位值时 fail closed。显式 HTTPS/反向代理地址始终优先。 +- `DISABLED` 输出 `gpgcheck=0`、`repo_gpgcheck=0` 且不输出 `gpgkey`。 +- `VERIFY_PACKAGE` 在 Phase 10 公钥注册和 public key 发布闭环完成前返回 + `ErrRepositoryGPGKeyUnavailable`。协议已预留 `gpgkey_urls` 列表支持多 key 与轮换,但不伪造 + 尚不存在的 key URL,也不提前启用 metadata signing。 + +### 19.3 Verification + +| Check | Result | Evidence | +| --- | --- | --- | +| Dynamic `.repo` | `PASS` | Generator 输出稳定 repo ID、文件名、MIME、完整文本及结构化 URL/GPG 元数据。 | +| Immutable mode | `PASS` | OC9 集成直接使用 `revisions//`,响应与 task active revision 一致。 | +| Real data-plane state | `PASS` | current 非 symlink、target 不一致、repomd 缺失或为空均拒绝生成。 | +| Public URL validation | `PASS` | 非 HTTP(S)、userinfo、query、fragment、CRLF 与空配置均 fail closed。 | +| GPG mapping | `PASS` | DISABLED 正确输出;VERIFY_PACKAGE 在无已发布公钥时 fail closed。 | +| Product-chain DNF | `PASS` | 真实 OC9 9.6 使用 Generator 原始 content,禁用其他 repo 后 `dnf makecache` 成功。 | +| Snapshot failure | `PASS` | 后续不可达同步 FAILED,已生成配置绑定的 immutable revision 不变。 | +| API/protocol | `PASS` | tRPC、mock、Swagger 独立生成且二次生成 SHA-256 一致;controller 字段映射通过。 | +| Build/vet | `PASS` | storage、domain、worker、application 定向 test/vet 和两个后端入口 build 通过。 | +| Deploy/Compose | `PASS` | footstone/worker 注入相同公开 URL;推导成功与占位地址拒绝均有测试。 | + +### 19.4 Gate Decision + +Phase 5 为 `PASS / READY FOR PHASE 6`,不是 `FINAL READY`。Phase 6 应按冻结的 Vue 3、Pinia、 +TDesign Vue Next 技术边界实现按 Product/System -> Version 组织的 Repository 页面,并接入本阶段 +Generator 的 Generate、Copy、Download。Phase 7 之后仍需三类隔离安装、诊断、单目标/多目标分发、GPG +完整闭环、总验收与完整恢复,因此 AC-31 `Release Acceptance` 继续为 `IN_PROGRESS`。 + +Phase 5 Evidence 位于 `.run/repository-poc/evidence/phase5/`。 + +## 20. Phase 6 Execution Record + +Phase 6 严格复用现有 Vue 3、Vue Router、Pinia、TDesign Vue Next 和 `fetchApi`,未新增 UI framework、 +状态管理或 HTTP client。页面直接调用 RepositoryService 的 20 个现有 tRPC HTTP gateway 方法,不建立 +平行 REST 或文件写入通道。 + +### 20.1 Product Surface + +- Repository List 按 `system_product -> product_version` 分组,并支持产品、版本、类型、架构和关键字筛选。 +- Create/Edit 表单覆盖 type、architecture、upstream、include/exclude、allow upload、enabled 和 GPG policy。 +- Repository Detail 提供 Overview、Packages、Upload、Quarantine、Tasks 和 Repo Config;NEVRA 删除创建异步任务。 +- Upload 使用 Begin/Append/Complete/Abort API,客户端分块计算 SHA-256;普通 HTTP 场景不依赖 Web Crypto + secure context,tar 的 `PARTIAL_SUCCESS` 使用 warning 视觉,不显示为绿色 Success。 +- Task Detail 展示步骤、Revision、尝试次数、错误码和错误信息,并提供符合状态约束的 cancel/retry 操作。 +- Repo Config 直接消费 Phase 5 Generator,提供 current/immutable revision、Generate、Copy 和 Download, + GPG 字段完全来自后端真实 policy。 +- footstone 菜单增加 `/repository/list`,详情路由隐藏;移动端只对 Repository 路由解除既有全局最小宽度, + 不改变其他产品模块的布局行为。 + +### 20.2 Verification + +| Check | Result | Evidence | +| --- | --- | --- | +| Repository API contract | `PASS` | 单元测试逐一核验 20 个 client method 对应现有 RepositoryService RPC 路径。 | +| Product/version organization | `PASS` | 分组/标签单元测试与桌面浏览器列表截图通过。 | +| Upload helpers | `PASS` | RPM/tar.gz 扩展名、binary base64、SHA-256 固定向量通过,共 14 个聚焦测试全部 PASS。 | +| PARTIAL_SUCCESS | `PASS` | warning 标签、任务抽屉和 `PARTIAL_INGESTION` 证据可见。 | +| Generator UI | `PASS` | Generate 返回的 `baseurl/gpgcheck/gpgkey/repo_gpgcheck` 原文展示,Copy/Download 按钮状态正确。 | +| Backend menu | `PASS` | OC9 `go test` 验证 `/repository -> /repository/list` 菜单可发现。 | +| Production build | `PASS` | `npm run build` 完成 6413 modules transform,Repository list/detail 独立异步 chunk 生成。 | +| Browser desktop | `PASS` | Chrome 1440x900 无 document/body 水平溢出,列表到详情的真实路由跳转通过。 | +| Browser mobile | `PASS` | Chrome 390x844 下工具栏与创建按钮完整位于 viewport,表格在页面容器内滚动。 | +| Existing full Vitest | `NON-BLOCKER` | 81 tests PASS;监控模块因既有未声明 `@microsoft/fetch-event-source` 有 1 suite load failure,与 Repository diff 无关。 | + +浏览器验证使用与当前 proto 一致的 API response mock,只证明前端交互、路由和产品契约;它不替代 Phase 7 +在真实 OC9/OCManager 环境中的 Case A/B/C 隔离安装。因此本阶段不得据此宣称 DNF E2E 已完成。 + +### 20.3 Gate Decision + +Phase 6 为 `PASS / READY FOR PHASE 7`,不是 `FINAL READY`。Phase 7 必须使用产品 Generator 输出的 +`.repo` 完成三个 Repository Isolation 安装用例和量化报告。Phase 8/9/10/11/12 的诊断、单目标/多目标分发、 +GPG 完整闭环、聚合自动化与恢复仍未完成,因此 AC-31 `Release Acceptance` 继续为 `IN_PROGRESS`。 + +Phase 6 Evidence 位于 `.run/repository-poc/evidence/phase6/`。 + +## 21. Phase 7 Execution Record + +Phase 7 将三个可信 DNF 用例落实为可重复执行的 `make -C manager repo-e2e`。测试不直接创建 +public 目录或手写 `.repo`:Repository 通过现有 Repository Service 创建,Sync/Upload 通过 +Content Service 创建异步任务,repository-worker 生成 repodata 并发布 immutable Revision,最后由 +产品 Generator 产生每个容器实际挂载的配置文件。 + +### 21.1 Isolation And Fixtures + +- `ocm-sync-only` 和 `ocm-sync-dependency` 只进入 Sync Repository。 +- `ocm-private-only` 和依赖 `ocm-sync-dependency` 的 `ocm-private-app` 只通过管理面上传进入 + Private Repository。 +- Case A、B、C 分别启动独立的 OpenCloudOS 9.6 disposable container;每个命令都显式使用 + `--disablerepo='*'` 和所需的 `--enablerepo`。 +- 测试使用 Generator 的 exact Revision URL,并在报告中固化 repository ID、revision ID、 + manifest SHA-256、repo ID 和 baseurl。 + +### 21.2 Quantitative Result + +| Case | Enabled Repository | Installed Package | Provider/Source | Result | +| --- | --- | --- | --- | --- | +| A | Sync only | `ocm-sync-only-0:1.0.0-1.oc9.noarch` | Sync repo ID | `PASS` | +| B | Private only | `ocm-private-only-0:1.0.0-1.oc9.noarch` | Private repo ID | `PASS` | +| C | Sync + Private | `ocm-private-app` + `ocm-sync-dependency` | app=Private,dependency=Sync | `PASS` | + +三项 DNF exit code 均为 0;`repoquery` 记录的 NEVRA/source repo 与容器内 `rpm -q` 一致。 +报告同时保存实际 DNF 命令、耗时、failed packages、日志摘要、日志 SHA-256 和原始日志路径。 + +### 21.3 Implementation And Verification + +- Go E2E:`manager/backend/services/footstone/repositoryworker/dnf_e2e_integration_test.go`。 +- RPM fixtures:`manager/backend/services/footstone/repositoryworker/testdata/e2e/`。 +- 自动化入口:`scripts/repository-e2e.sh` 和 `manager/Makefile` 的 `repo-e2e` target。 +- OC9 9.6 正式 Makefile 入口连续通过,最终权威运行 3/3 PASS。 +- repositoryworker 全包测试、`go vet`、Shell syntax、Generator `.repo` 内容和 Evidence SHA-256 + 校验均为 PASS。 + +### 21.4 Gate Decision + +Phase 7 为 `PASS / READY FOR PHASE 8`,不是 `FINAL READY`。Phase 8 的失败诊断、Phase 9 的 +单目标/多目标产品分发、Phase 10 的 GPG 完整闭环以及 Phase 11-12 的聚合验收和恢复仍是发布阻断项, +因此 AC-31 `Release Acceptance` 继续为 `IN_PROGRESS`。 + +Phase 7 Evidence 位于 `.run/repository-poc/evidence/phase7/`。 + +## 22. Phase 8 Execution Record + +Phase 8 将同步、上传和 DNF Validation 的失败归因收敛为同一套确定性规则,不依赖 LLM 或外部 AI +服务。领域逻辑位于 `manager/backend/services/footstone/domain/repositoryDiagnosisService/`;任务完成或 +租约恢复进入终态时,`repositoryTaskService` 生成并持久化结构化诊断。任务数据只增加直接相关的诊断列, +对应迁移为 `v1.0.0_9_repository_diagnosis.sql`,没有引入新的数据库或旁路日志存储。 + +### 22.1 Product Contract + +- `RepositoryTask.diagnosis` 返回 Failure Category、Error Code、Failed Step、Likely Cause、Evidence、 + Recommended Action、Rule ID 和生成时间。 +- `ExportRepositoryTaskDiagnosis` 导出 `ocmanager.repository.diagnosis/v1` JSON,并返回内容 SHA-256。 +- Task Detail 使用 warning 视觉显示失败类别、步骤、原因、建议和 Evidence,支持直接下载报告; + `PARTIAL_SUCCESS` 继续与普通绿色 Success 区分。 +- 同步规则覆盖 DNS、网络、HTTP 4xx/5xx、metadata、download、checksum、architecture 和过滤空结果; + 上传覆盖 damaged/invalid RPM、GPG、wrong arch、NEVRA conflict、dangerous archive 和部分拒绝; + Validation 覆盖仓库不可达、包缺失、依赖缺失/冲突、metadata 和 GPG。 +- Authorization/Bearer、URL userinfo、password、token、secret 和 API key 在入库与 Evidence 生成前脱敏。 + +### 22.2 Verification + +| Check | Result | Evidence | +| --- | --- | --- | +| Rule matrix | `PASS` | 22/22,三条产品链路均有正向匹配和 fallback。 | +| Evidence redaction | `PASS` | 错误信息、current step、持久化记录和导出报告均无测试凭据。 | +| Persistence/export | `PASS` | MariaDB 集成验证 `DNF_DEPENDENCY_MISSING` 保存、读取和 SHA-256 JSON 导出。 | +| Migration | `PASS` | 从 Phase 2 任务表基线执行 migration,7/7 诊断列存在。 | +| Repository API | `PASS` | 独立 proto/trpc/mock/Swagger 生成,controller 映射测试通过。 | +| Frontend | `PASS` | Repository Vitest 14/14、聚焦 ESLint 和 Vite test build 通过。 | +| Backend regression | `PASS` | `go build ./...`、repositoryworker 全包测试和聚焦 `go vet` 通过。 | + +### 22.3 Gate Decision + +Phase 8 为 `PASS / READY FOR PHASE 9`,不是 `FINAL READY`。Phase 9 的 immutable 单目标/多目标分发、 +Phase 10 的 GPG 完整闭环,以及 Phase 11-12 的聚合验收与恢复仍是发布阻断项;AC-31 +`Release Acceptance` 继续保持 `IN_PROGRESS`。 + +Phase 8 自动化入口为 `make -C manager repo-diagnosis`,Evidence 位于 +`.run/repository-poc/evidence/phase8/`。 + +## 23. Phase 9 Execution Record + +Phase 9 复用了当前仓库内既有 TMS API 与官方 Agent 协议,没有修改或重新构建外部 Agent。Repository +新增 Distribution model、MySQL migration、4 个管理面 RPC、TMS client、worker handler 和 Vue 详情页。 +用户只能选择 TMS 已注册节点;离线节点不可提交。每个任务保存绑定的 immutable Revision、manifest +与 repomd 摘要,并分别记录目标节点状态、Agent ID、attempt、exit code 和脱敏 output。 + +### 23.1 Direct Blockers And Minimum Fixes + +| Finding | Classification | Evidence | Minimum required fix | Regression scope | +| --- | --- | --- | --- | --- | +| TMS CA 缺少 CRL 签发用途 | `BLOCKER -> RESOLVED` | 现有 `init-ca.sh` 生成证书不能满足 Agent CRL 校验;修复后 OpenSSL 显示 `CA:TRUE`、`Certificate Sign, CRL Sign` | 只为新 CA 增加 critical basic constraints 与 key usage | TMS disposable CA 生成及 Agent mTLS 链路 | +| `/assets/query` 拒绝 `agent_id=0` | `BLOCKER -> RESOLVED` | 产品节点选择必须列出已注册资产;底层 asset service 已支持 0 查询全量 | 移除 API facade 的一处提前拒绝并更新定向测试 | TMS assets query handler | +| TMS health 中既有 file-service MD5 告警 | `NON-BLOCKER` | 单目标和多目标正负向 Distribution 均可完成,未影响 artifact 执行或回执 | 不修改 | 无 Repository 变更 | +| Presentation 全包既有 cluster nil 测试 | `NON-BLOCKER` | Repository 定向 presentation 测试、build 和 vet 均通过;失败文件不在本次 diff | 不修改 | 无 Repository 变更 | + +### 23.2 Product Contract + +- `ListRepositoryDistributionNodes` 合并 TMS assets 与 Agent connection status,Agent ID 在协议层使用字符串, + 避免浏览器丢失 uint64 精度。 +- `CreateRepositoryDistribution` 在事务中固化 Repository、published Revision、manifest/repomd 摘要和目标。 +- worker 上传的 TMS artifact 只包含确定性 `run.sh`;脚本不读取 `current/latest`,只读取精确 Revision URL。 +- 目标节点使用 `staging -> revisions/ -> relative current symlink`,已存在的同 ID Revision 也必须 + 重新校验后才能复用。 +- 中央 worker 只有在 TMS target 成功、exit code 为 0、结果 marker 和两个摘要均一致时才标记目标成功; + 汇总状态为 `SUCCESS/PARTIAL_SUCCESS/FAILED`。 +- API 拒绝未注册或离线节点;worker 超时会主动取消 TMS task,TMS 取消终态保持 `CANCELED`, + 避免中央任务结束后远端仍继续切换。 +- Repository Detail 的“节点分发”页提供在线节点选择、创建、状态轮询和逐节点结果抽屉; + `PARTIAL_SUCCESS` 使用 warning 状态,不显示为普通绿色成功。 + +### 23.3 Verification + +| Check | Result | Evidence | +| --- | --- | --- | +| Backend unit/build/vet | `PASS` | TMS client、Distribution service、worker、presentation、application 定向测试及两个 Go 入口构建通过。 | +| TMS handler regression | `PASS` | `agent_id=0` list 与现有 IP resolver 定向测试通过。 | +| Frontend | `PASS` | Repository Vitest 14/14、聚焦 ESLint 和 Vite release build(6416 modules)通过。 | +| Browser | `PASS` | 真实 Chrome 验证在线/离线节点、创建、PARTIAL_SUCCESS、逐节点详情和 720px drawer 边界。 | +| Proto isolation | `PASS` | 既有生成流程只产生 Repository 的 pb/trpc/mock/Swagger 4 个预期生成物。 | +| MySQL migration | `PASS` | OC9 MariaDB disposable 数据库连续执行两次,2 张 InnoDB 表、主键和 6 个业务索引保持一致。 | +| Positive dual-node | `PASS` | 同一 immutable Revision 分发到两个独立 Agent,两目标 exit 0、attempt 1,manifest/repomd 摘要一致。 | +| Negative digest | `PASS` | 故意错误 manifest 摘要使两目标均失败,两个节点原 `current` 与文件清单保持不变。 | +| Environment restore | `PASS` | disposable MariaDB、TMS 容器、临时 API 与 Agent 验证进程均停止,正式数据未删除。 | + +### 23.4 Gate Decision + +Phase 9 与内部 AC-25 Multi-node Distribution 均为 `PASS / READY FOR PHASE 10`,不是 +`FINAL READY`。Phase 10 的 GPG 完整闭环、Phase 11 +的聚合自动化和导出、Phase 12 的完整恢复与最终回归仍是发布阻断项;AC-31 `Release Acceptance` +继续保持 `IN_PROGRESS`。 + +Phase 9 Evidence 位于 `.run/repository-poc/evidence/phase9/`,分发产品验收明细位于其 +`distribution/` 子目录。 + +## 24. Phase 10 GPG Complete Verification 执行记录 + +### 24.1 密钥与存储边界 + +- `repository_gpg_keys` 仅保存 `id/repository_id/key_id/fingerprint/public_key_path/created_at`, + 不保存 key body 或私钥。 +- 管理副本位于 `keys///public.key`,只读公开副本位于 + `public//keys//public.key`。 +- API 只接受单个 ASCII-armored OpenPGP public key block,拒绝私钥、多实体和超过 + 1 MiB 的输入;额外 armor 块直接拒绝,通过校验的实体在落盘前重新序列化为规范公钥。 + 有 active Revision 的 Repository 不允许删除授权 key,避免已生成的 + `.repo` 失去校验依据。 + +### 24.2 软件包签名校验 + +- worker 为每个 Task 创建隔离的 RPM keyring,只导入 DB 授权公钥,任务结束后清理。 +- `VERIFY_PACKAGE` 同时检查 RPM 签名字段和 `rpmkeys --checksig`;未签名、未授权 key + 和坏签名分别返回 `UNSIGNED_RPM`、`UNAUTHORIZED_GPG_KEY`、`BAD_SIGNATURE`。 +- 上传负向样例进入 quarantine,active Revision 不变;Remote Sync 按 snapshot 语义失败, + 不发布部分内容。 +- `VERIFY_PACKAGE` 没有可用授权 key 时 fail closed。Generator 从真实 policy 和公钥 + URL 生成 `gpgcheck=1`、`repo_gpgcheck=0` 与 `gpgkey=`。 + +### 24.3 产品交互 + +Repository Detail 的 GPG 页签支持选择、注册、查看和按约束删除授权公钥, +并展示 key ID、fingerprint、公开 URL 和创建时间。Repo Config 页签输出的长 +`gpgkey` URL 在桌面与 390px 移动视口中都会换行,不会撑开页面。 + +### 24.4 验证结果 + +| Check | Result | Evidence | +| --- | --- | --- | +| Backend unit/protocol/presentation | `PASS` | GPG key service、storage、worker、diagnosis、application 和 Repository controller 定向测试通过。 | +| Proto isolation | `PASS` | 既有流程只生成 Repository `pb/trpc/mock/Swagger` 四个直接相关文件。 | +| Authorized signed RPM | `PASS` | OC9 上传已授权 key 签名 RPM,成功发布,使用产品生成的 `.repo` 隔离安装成功。 | +| Unsigned/unauthorized RPM | `PASS` | 两类输入均进入 quarantine,原 active Revision 和 `repomd.xml` 摘要不变。 | +| Signed remote sync | `PASS` | 授权签名 snapshot 发布成功,未授权 snapshot 失败且不切换 active Revision。 | +| DB/private-key boundary | `PASS` | MariaDB schema 只有六个允许字段,public 目录与证据中无 private-key block。 | +| Frontend/browser | `PASS` | Repository Vitest 14/14、release build、Chrome 桌面/移动视口与 console 检查通过。 | +| Environment restore | `PASS` | disposable MariaDB 已停止,临时 DB/user 已由脚本 trap 删除,未改动正式数据。 | + +全包 presentation 测试仍会命中既有 cluster nil config panic,失败文件不在本次 +Repository diff 中,Repository 定向 presentation 测试已通过;该问题标记为 +`NON-BLOCKER`,不在本次扩大修改范围。 + +### 24.5 Gate Decision + +Phase 10 和 GPG 签名闭环为 `PASS / READY FOR PHASE 11`,不是 `FINAL READY`。Phase 11 的 +Make/Shell 聚合自动化、inventory/report export 和 AC-31 `Release Acceptance`,以及 Phase 12 +的完整恢复验收仍是发布阻断项。 + +Phase 10 Evidence 位于 `.run/repository-poc/evidence/phase10/`。 + +## 25. Phase 11 Automation And Export 执行记录 + +### 25.1 产品导出 + +RepositoryService 新增三个导出 RPC,均经过现有 application/domain 分层和项目权限校验: + +- `ExportRepositoryInventory` 按项目导出 Repository、active Revision、软件包 NEVRA/SHA-256、 + quarantine 数量和 GPG 公钥元数据。 +- `ExportRepositoryValidationReport` 导出 VALIDATE task 的状态、耗时、失败步骤和脱敏诊断。 +- `ExportRepositoryDistributionResult` 导出 Distribution 绑定的 immutable Revision、manifest/repomd + 摘要、TMS task 和逐节点结果。 + +三个 JSON 分别使用 `ocmanager.repository.inventory/v1`、 +`ocmanager.repository.validation/v1` 和 `ocmanager.repository.distribution/v1` schema,响应同时返回 +文件名、内容类型和 SHA-256。数据库不保存导出副本,报告按请求时的产品数据生成;日志和目标节点输出 +在进入报告前复用 Diagnosis 脱敏规则。 + +前端在仓库列表、仓库详情和分发详情中提供对应的下载按钮,继续使用现有 Vue 3、TDesign 和 +`fetchApi`。真实 Chrome 已读取三个下载文件并校验 schema;分发抽屉增加 `max-width: 100vw` 和 +小屏单列布局,390px 视口下长 ID 与摘要完整可见。 + +### 25.2 正式自动化入口 + +`manager/Makefile` 目前提供: + +```text +repo-e2e +repo-diagnosis +repo-report +repo-gpg +repo-distribution +repo-acceptance +``` + +`repository-gpg.sh` 在 OC9 上现场生成授权与未授权临时 key,完成签名上传、隔离安装、未签名/未授权 +quarantine 和签名同步测试;私钥只存在于 `mktemp` 目录,退出时删除。`repository-distribution.sh` +从 TMS `/assets/query` 动态解析一个或多个 Agent ID,创建最小 immutable Repository,验证正向分发和 +错误 manifest 摘要负向用例,不再依赖历史 Agent ID 或历史 Revision。 + +内部矩阵保存在 `scripts/repository-acceptance-matrix.json`。`repository-acceptance.sh` 从矩阵动态计算 +总项数和 9 类展示结果;阶段复用模式只用于检查既有 Evidence 的连贯性,永远不能让 AC-31 通过。 +默认完整模式会执行环境检查、Compose 配置、后端/前端回归、DNF、诊断、GPG、分发和产品报告, +任何 MUST 未通过均返回非零。 + +### 25.3 验证结果 + +| Check | Result | Evidence | +| --- | --- | --- | +| Product report integration | `PASS` | OC9 + MariaDB 生成 inventory、validation、distribution 三份真实 JSON,schema、摘要与脱敏检查通过。 | +| Formal GPG runner | `PASS` | 授权签名安装、未签名/未授权隔离、签名同步和 private-key absence 全部通过。 | +| Formal distribution runner | `PASS` | 动态发现目标 Agent;正向目标均 exit 0,错误 manifest 时目标均失败且不切换 active Revision;历史环境另完成了双目标增强验证。 | +| Frontend | `PASS` | Repository Vitest 4 files/14 tests,Vite release build 6419 modules。 | +| Browser | `PASS` | 三个产品下载、JSON schema、桌面/390px 布局和 console 检查通过。 | +| Aggregate gate | `PASS` | 阶段复用检查得到 29/31 PASS、0 FAIL、2 IN_PROGRESS,并按设计返回非零。 | +| Proto isolation | `PASS` | Repository proto 独立生成仍只变化 pb/trpc/mock/Swagger 四个直接相关文件。 | + +### 25.4 Gate Decision + +Phase 11 为 `PASS / READY FOR PHASE 12`,不是 `FINAL READY`。当前细粒度矩阵为 29/31 PASS; +AC-29 需要在 disposable OC9 环境完成 restart、redeploy、destroy/restore 后的完整复现,AC-31 必须在 +该恢复流程中再次运行默认 `repo-acceptance` 才能通过。任一项未通过,项目仍不得标记 `FINAL READY`。 + +Phase 11 Evidence 位于 `.run/repository-poc/evidence/phase11/`。 + +## 26. Phase 12 Recovery And Release Acceptance 执行记录 + +### 26.1 实现范围 + +Phase 12 增加三层恢复和最终聚合入口,没有改变已冻结的产品架构: + +```text +scripts/repository-recovery.compose.yml +scripts/repository-recovery.nginx.conf +scripts/repository-recovery.sh +scripts/repository-tms-recovery.sh +scripts/repository-release-acceptance.sh +``` + +`repo-recovery` 分别验证产品数据恢复和 TMS 重启后的单目标或多目标任务; +`repo-basic-acceptance` 验证不启用 OCAI、Repository Node 或 Agent 时的基础部署边界与当前服务健康状态; +`repo-acceptance-matrix` 执行并聚合 AC-01 至 AC-31;`repo-acceptance` 先完成恢复,再运行完整矩阵, +是最终对外验收入口。 + +提交前审计将 TMS 恢复的默认输入改为当前仓库已有的 +`tms/deploy/docker-compose.yaml + infra/tms.override.yml + config/.env`,Compose project 与 +`scripts/deploy.sh` 一致使用 `oc-tms`。显式 control-root 仍可用于已部署环境,不再把 `.run` +中的 POC 目录作为干净克隆的必要条件。同时,recovery POC 的 DB/JWT 凭据在每次运行时随机生成, +Compose 校验使用静默模式,Evidence 不保存展开后的环境变量。 + +### 26.2 产品恢复结果 + +恢复环境使用 OpenCloudOS 9.6、MariaDB、真实 footstone/repository-worker 镜像与只读 Nginx。 +Repository bind root 和数据库备份只在 disposable 目录内创建和销毁。 + +| Check | Result | Evidence | +| --- | --- | --- | +| Restart | `PASS` | API、worker、Nginx 重启后 current Revision、HTTP 和 lease recovery 通过。 | +| Redeploy | `PASS` | 重建应用容器、保留 bind data/MySQL 后状态一致。 | +| Destroy/Restore | `PASS` | 从 MySQL backup、bind backup 和 manifest 恢复后状态一致。 | +| State identity | `PASS` | 提交前复跑的 baseline/restart/redeploy/restore 归一化摘要均为 `aa259f4e24a08b720622898656d9cb36bf9a394c1498464a7c7d53775c9cd2a0`。 | +| Repository semantics | `PASS` | Repository ID、active Revision、manifest、inventory、GPG metadata 和 Validation Report 均一致。 | + +### 26.3 TMS 恢复边界 + +现有 TMS/Agent 链路支持整套 TMS 在空闲边界重启,所请求的 Agent 重连后再执行单目标或多目标任务。 +提交前历史复跑使用两个独立 Agent,两个目标均返回 stdout、exit status 和成功结果。 + +“分发任务执行中重启全部 TMS 服务”的负向试验中,两节点返回 +`sent_ack_timeout`。因此本方案不将“执行中整套 TMS 无损重启”列为已实现能力。正式操作边界为: +整套 TMS 只在无进行中分发任务时重启,等待 Agent 重连后再创建任务,失败任务通过产品重试。 +Repository worker 的 lease recovery 由独立用例验证通过。 + +### 26.4 V3.1 历史 Release Acceptance 结果 + +在三层恢复完成后,OpenCloudOS 9.6 上执行: + +```bash +make -C manager repo-acceptance +``` + +后端 Repository 回归、前端 4 个测试文件/14 个用例、6419 个模块的生产构建、 +Case A/B/C、GPG 正负向、失败诊断、单目标/多目标 immutable distribution 及三类报告导出全部通过。 +最终摘要为: + +```text +Repository Acceptance +31/31 PASS +0 failed +0 pending +Release Acceptance: PASS +``` + +该结果仅对应 V3.1 旧拓扑,当时 9 个对外分类 Core/Enhanced 均为 `PASS`。V4.0 引入独立 +Repository Node、远程任务数据面、容量预检、大日志治理和 OCAI 后,必须在新拓扑重新聚合, +不能沿用此处的 `FINAL READY` 结论。 + +Phase 12 提交前最终 Evidence 位于 +`.run/repository-poc/evidence/phase12/submission-readiness-final/`;恢复与 Release Acceptance 两份 +`SHA256SUMS` 已在本地通过校验。执行中重启整套 TMS 的负向记录单独保存在 +`.run/repository-poc/evidence/phase12/tms-negative-inflight-full-restart/`。 + +### 26.5 V4.0 当前 Release Acceptance + +V4.0 在 9 月 1 日的阶段 Evidence 位于 `.run/repository-poc/evidence/v4-p9-20260901/`;当时 +`28/31 PASS` 的结果已被后续 Build98 全量验收取代。Build98 的实际 OCAI SSE、恢复和新拓扑 +Release Acceptance 已达到 31/31 PASS,证据位于 +`.run/repository-poc/evidence/v4-final-20260903/20260903-final-build98-v2/`。 + +### 26.6 Build99 真实节点分发补强 + +Build99 修复远端无数据库 worker 未路由 `DISTRIBUTE` 的问题,新增内部 mTLS 执行 RPC,由管理面 +继续复用现有 TMS/Agent 完成分发。验证使用 OpenCloudOS 官方源中的 4 个 RPM,共 +154,619,176 字节,固定绑定 Repository、Revision、manifest SHA-256 和 repomd SHA-256,从 +独立仓库节点分发到另一个 TMS Agent。 + +首轮分发与 Footstone/worker 重启后的幂等复测均为 `SUCCESS`。目标节点的 `current` 为相对链接, +manifest 中 11 个文件逐项校验通过,DNF 在禁用全部默认源后仅从该 Revision 读取 4 个包。 +Repository 定向测试和 `go build ./...` 通过,Evidence 位于 +`.run/repository-poc/evidence/v4-final-20260904/20260904-build99-real-distribution-closure/`。 + +Build99 的功能修复已纳入 Build100 全量验收,不再单独作为发布结论。 + +### 26.7 Build100 最终 Release Acceptance + +Build100 使用版本化源码快照重新执行协议生成,Repository proto 生成前后零差异;Repository 后端定向 +测试、`go build ./...`、前端 20 项测试和生产构建均通过。完整验收同时覆盖真实 OCAI、1 GiB 日志治理、 +GPG 正反向用例、隔离 DNF Case A/B/C、官方 RPM Revision 节点间分发、HTTP 只读策略,以及 +restart、redeploy、destroy/restore 三层恢复。 + +最终 AC-01 至 AC-31 为 `31/31 PASS`,6 个 Core 与 3 个 Enhanced 分类全部为 `PASS`, +`Release Acceptance` 和 `final_ready` 均为真。最终 Evidence 位于 +`.run/repository-poc/evidence/v4-final-20260904/20260904-build100-full-v3/`,其中包含 +`release-acceptance/summary.json`、31 项明细、分类汇总、恢复结果和 Evidence Bundle。 + +此前 full-v1 的镜像源 DNS 失败和 full-v2 的非交互 sudo 失败作为环境负向记录保留,均未替代产品验收; +full-v3 在相同 Build100 代码和既定拓扑上完成全部门禁,因此 Build100 当时达到 `FINAL READY`。 +该结论作为历史 Evidence 保留,不替代 Build111 至 Build115 增量修复后的最终全量验收。 diff --git a/docs/evidence/repository-acceptance-20261014.json b/docs/evidence/repository-acceptance-20261014.json new file mode 100644 index 0000000000000000000000000000000000000000..da4b79720e0297748712faeac498adfdd66115f0 --- /dev/null +++ b/docs/evidence/repository-acceptance-20261014.json @@ -0,0 +1,6171 @@ +{ + "schema_version": 1, + "record_kind": "historical_verified_candidate_summary", + "verified_at": "2026-10-14T11:55:16Z", + "description": "Published subset of the verified candidate evidence. Reading this file does not execute tests. Raw command logs and private environment details remain in the local evidence archive.", + "criteria_file": "manager/acceptance/repository-acceptance-criteria.json", + "criteria_sha256": "f6c4fe33b786560521621ef192e98387af8a9b056031e9f94148c4395e278d1f", + "source_results_sha256": "29fbb5b6eb5486ee8f6eacd098f10f7a50b69a9eec00b022d5d7dec0d2df7589", + "acceptance": { + "mode": "strict", + "counts": { + "expected_ids": 152, + "unique_expected_ids": 152, + "actual_ids": 152, + "unique_actual_ids": 152, + "duplicate_ids": 0, + "missing_ids": 0, + "unexpected_ids": 0, + "non_pass_ids": 0, + "failed_count": 0, + "passed_count": 152, + "incomplete_evidence_ids": 0 + }, + "details": { + "missing": [], + "unexpected": [], + "duplicates": [], + "non_pass": [], + "incomplete_evidence": [] + }, + "passed": true + }, + "core": { + "status": "PASS", + "mode": "full", + "totals": { + "passed": 31, + "failed": 0, + "blocked": 0, + "pending": 0, + "total": 31 + } + }, + "bundles": { + "status": "PASS", + "totals": { + "categories": 9, + "passed": 9, + "failed": 0, + "blocked": 0, + "pending": 0 + }, + "categories": [ + { + "group": "Core", + "category": "Automation", + "status": "PASS", + "totals": { + "total": 6, + "passed": 6, + "failed": 0, + "blocked": 0, + "pending": 0 + } + }, + { + "group": "Core", + "category": "DNF Validation", + "status": "PASS", + "totals": { + "total": 4, + "passed": 4, + "failed": 0, + "blocked": 0, + "pending": 0 + } + }, + { + "group": "Core", + "category": "Package Ingestion", + "status": "PASS", + "totals": { + "total": 7, + "passed": 7, + "failed": 0, + "blocked": 0, + "pending": 0 + } + }, + { + "group": "Core", + "category": "Repository Management", + "status": "PASS", + "totals": { + "total": 7, + "passed": 7, + "failed": 0, + "blocked": 0, + "pending": 0 + } + }, + { + "group": "Core", + "category": "Repository Publishing", + "status": "PASS", + "totals": { + "total": 3, + "passed": 3, + "failed": 0, + "blocked": 0, + "pending": 0 + } + }, + { + "group": "Core", + "category": "Security", + "status": "PASS", + "totals": { + "total": 1, + "passed": 1, + "failed": 0, + "blocked": 0, + "pending": 0 + } + }, + { + "group": "Enhanced", + "category": "Diagnosis", + "status": "PASS", + "totals": { + "total": 1, + "passed": 1, + "failed": 0, + "blocked": 0, + "pending": 0 + } + }, + { + "group": "Enhanced", + "category": "Distribution", + "status": "PASS", + "totals": { + "total": 1, + "passed": 1, + "failed": 0, + "blocked": 0, + "pending": 0 + } + }, + { + "group": "Enhanced", + "category": "Repo Generator", + "status": "PASS", + "totals": { + "total": 1, + "passed": 1, + "failed": 0, + "blocked": 0, + "pending": 0 + } + } + ] + }, + "recovery": { + "status": "PASS", + "checks": { + "restart": "PASS", + "redeploy": "PASS", + "destroy_restore": "PASS", + "tms_restart": "PASS", + "acceptance_after_restore": "PASS", + "evidence_bundles": "PASS" + } + }, + "exit_code": 0, + "items": [ + { + "id": "1", + "requirement": "软件仓库主页面布局完成重排:保持现有配色、组件库、字体、交互风格,不更换整体 UI 风格,不引入新的前端框架、字体或色板。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/service/repository.ts" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 5, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "9d79bb74854749a82cf4cf2e4f1daca9443922211865ecfc3b208de3c6101439" + }, + { + "ordinal": 2, + "sha256": "cd834effe24011a17809a39dc72c4970768bf6042b567262c370b42edbc11288" + }, + { + "ordinal": 3, + "sha256": "dfe2506e874da8060f4c2c322f0b9ca9e4bde932d459e494ee2f22da3be36674" + }, + { + "ordinal": 4, + "sha256": "9df2cd0239a9ba51d34cff9abca40ae9731198ca53f44c6634c3e69244f6ad49" + }, + { + "ordinal": 5, + "sha256": "db13c131380dfa9385416f69bb2ec546c2e94fd6d0ad1d526d4157da4af73272" + } + ] + }, + { + "id": "2", + "requirement": "主页面查询区域重新排版:查询条件宽度合理、控件高度统一、Label 对齐,消除右侧大面积无意义空白;窄屏时能够自然换行,不出现按钮挤压和输入框异常拉伸。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/service/repository.ts" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 5, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "9d79bb74854749a82cf4cf2e4f1daca9443922211865ecfc3b208de3c6101439" + }, + { + "ordinal": 2, + "sha256": "cd834effe24011a17809a39dc72c4970768bf6042b567262c370b42edbc11288" + }, + { + "ordinal": 3, + "sha256": "dfe2506e874da8060f4c2c322f0b9ca9e4bde932d459e494ee2f22da3be36674" + }, + { + "ordinal": 4, + "sha256": "9df2cd0239a9ba51d34cff9abca40ae9731198ca53f44c6634c3e69244f6ad49" + }, + { + "ordinal": 5, + "sha256": "db13c131380dfa9385416f69bb2ec546c2e94fd6d0ad1d526d4157da4af73272" + } + ] + }, + { + "id": "3", + "requirement": "仓库列表列宽重新分配:仓库名称列不再过宽,其余字段不再严重挤压;长名称使用省略号,并可通过 Tooltip 查看完整内容。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/service/repository.ts" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 5, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "9d79bb74854749a82cf4cf2e4f1daca9443922211865ecfc3b208de3c6101439" + }, + { + "ordinal": 2, + "sha256": "cd834effe24011a17809a39dc72c4970768bf6042b567262c370b42edbc11288" + }, + { + "ordinal": 3, + "sha256": "dfe2506e874da8060f4c2c322f0b9ca9e4bde932d459e494ee2f22da3be36674" + }, + { + "ordinal": 4, + "sha256": "9df2cd0239a9ba51d34cff9abca40ae9731198ca53f44c6634c3e69244f6ad49" + }, + { + "ordinal": 5, + "sha256": "db13c131380dfa9385416f69bb2ec546c2e94fd6d0ad1d526d4157da4af73272" + } + ] + }, + { + "id": "4", + "requirement": "仓库列表取消低频“创建人”核心列:创建人等低频信息移入详情或次级信息展示,不再长期占据主列表空间。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/service/repository.ts" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 5, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "9d79bb74854749a82cf4cf2e4f1daca9443922211865ecfc3b208de3c6101439" + }, + { + "ordinal": 2, + "sha256": "cd834effe24011a17809a39dc72c4970768bf6042b567262c370b42edbc11288" + }, + { + "ordinal": 3, + "sha256": "dfe2506e874da8060f4c2c322f0b9ca9e4bde932d459e494ee2f22da3be36674" + }, + { + "ordinal": 4, + "sha256": "9df2cd0239a9ba51d34cff9abca40ae9731198ca53f44c6634c3e69244f6ad49" + }, + { + "ordinal": 5, + "sha256": "db13c131380dfa9385416f69bb2ec546c2e94fd6d0ad1d526d4157da4af73272" + } + ] + }, + { + "id": "5", + "requirement": "仓库列表新增高频运维字段:必须直接展示包数量、仓库总体积、最近同步时间,不需要进入仓库详情才能查看。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/service/repository.ts" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 5, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "9d79bb74854749a82cf4cf2e4f1daca9443922211865ecfc3b208de3c6101439" + }, + { + "ordinal": 2, + "sha256": "cd834effe24011a17809a39dc72c4970768bf6042b567262c370b42edbc11288" + }, + { + "ordinal": 3, + "sha256": "dfe2506e874da8060f4c2c322f0b9ca9e4bde932d459e494ee2f22da3be36674" + }, + { + "ordinal": 4, + "sha256": "9df2cd0239a9ba51d34cff9abca40ae9731198ca53f44c6634c3e69244f6ad49" + }, + { + "ordinal": 5, + "sha256": "db13c131380dfa9385416f69bb2ec546c2e94fd6d0ad1d526d4157da4af73272" + } + ] + }, + { + "id": "6", + "requirement": "仓库列表字段视觉统一:数字、时间、状态、标签等对齐合理;同类字段宽度稳定,不因内容长短造成明显跳动。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/service/repository.ts" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 5, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "9d79bb74854749a82cf4cf2e4f1daca9443922211865ecfc3b208de3c6101439" + }, + { + "ordinal": 2, + "sha256": "cd834effe24011a17809a39dc72c4970768bf6042b567262c370b42edbc11288" + }, + { + "ordinal": 3, + "sha256": "dfe2506e874da8060f4c2c322f0b9ca9e4bde932d459e494ee2f22da3be36674" + }, + { + "ordinal": 4, + "sha256": "9df2cd0239a9ba51d34cff9abca40ae9731198ca53f44c6634c3e69244f6ad49" + }, + { + "ordinal": 5, + "sha256": "db13c131380dfa9385416f69bb2ec546c2e94fd6d0ad1d526d4157da4af73272" + } + ] + }, + { + "id": "7", + "requirement": "解决“系统/产品+版本分组”和后端分页冲突:不能继续出现某个分组在当前页只剩一行、视觉上被分页截断的情况。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/service/repository.ts" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 5, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "9d79bb74854749a82cf4cf2e4f1daca9443922211865ecfc3b208de3c6101439" + }, + { + "ordinal": 2, + "sha256": "cd834effe24011a17809a39dc72c4970768bf6042b567262c370b42edbc11288" + }, + { + "ordinal": 3, + "sha256": "dfe2506e874da8060f4c2c322f0b9ca9e4bde932d459e494ee2f22da3be36674" + }, + { + "ordinal": 4, + "sha256": "9df2cd0239a9ba51d34cff9abca40ae9731198ca53f44c6634c3e69244f6ad49" + }, + { + "ordinal": 5, + "sha256": "db13c131380dfa9385416f69bb2ec546c2e94fd6d0ad1d526d4157da4af73272" + } + ] + }, + { + "id": "8", + "requirement": "主列表分页方案最终确定并实现:优先采用平铺分页表格,将系统/产品和版本作为明确字段或标签展示,通过筛选实现逻辑分组;不得为了视觉分组破坏现有后端分页能力。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/service/repository.ts" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 5, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "9d79bb74854749a82cf4cf2e4f1daca9443922211865ecfc3b208de3c6101439" + }, + { + "ordinal": 2, + "sha256": "cd834effe24011a17809a39dc72c4970768bf6042b567262c370b42edbc11288" + }, + { + "ordinal": 3, + "sha256": "dfe2506e874da8060f4c2c322f0b9ca9e4bde932d459e494ee2f22da3be36674" + }, + { + "ordinal": 4, + "sha256": "9df2cd0239a9ba51d34cff9abca40ae9731198ca53f44c6634c3e69244f6ad49" + }, + { + "ordinal": 5, + "sha256": "db13c131380dfa9385416f69bb2ec546c2e94fd6d0ad1d526d4157da4af73272" + } + ] + }, + { + "id": "9", + "requirement": "新增一级“任务总览”页面:任务页面不能再只能从单仓库详情进入。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/tasks.vue", + "manager/backend/services/footstone/application/repository_task.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/application/repository_task_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go", + "manager/backend/services/footstone/presentation/repository_task_controller_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 5, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "aae50fd750d54a8181d33fe1b12aac83d9e60bcf80413098485ce62c18e53859" + }, + { + "ordinal": 2, + "sha256": "80002c536e6ba09a4b7ad0349162d2472fb0231b9bf6f321345fb85f094b288d" + }, + { + "ordinal": 3, + "sha256": "8685d020c0525fdca2bd4bfd7058eb7064a673bc092f2330b174786f8886c856" + }, + { + "ordinal": 4, + "sha256": "fd69decbdb6dbb1001717b7c265599342f24ebba0efd480bef1b07e95981731a" + }, + { + "ordinal": 5, + "sha256": "d4f0dfc1f0b27dc5f4c0270f82a4b5d74d524d8a13269391763fd091ecbee5e7" + } + ] + }, + { + "id": "10", + "requirement": "一级菜单层级调整完成:至少形成“软件仓库 → 任务总览 → 仓库节点 → 上游源”的一级导航结构;上游源原有页面内容不做无必要改版。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/tasks.vue", + "manager/backend/services/footstone/application/repository_task.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/application/repository_task_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go", + "manager/backend/services/footstone/presentation/repository_task_controller_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 5, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "aae50fd750d54a8181d33fe1b12aac83d9e60bcf80413098485ce62c18e53859" + }, + { + "ordinal": 2, + "sha256": "80002c536e6ba09a4b7ad0349162d2472fb0231b9bf6f321345fb85f094b288d" + }, + { + "ordinal": 3, + "sha256": "8685d020c0525fdca2bd4bfd7058eb7064a673bc092f2330b174786f8886c856" + }, + { + "ordinal": 4, + "sha256": "fd69decbdb6dbb1001717b7c265599342f24ebba0efd480bef1b07e95981731a" + }, + { + "ordinal": 5, + "sha256": "d4f0dfc1f0b27dc5f4c0270f82a4b5d74d524d8a13269391763fd091ecbee5e7" + } + ] + }, + { + "id": "11", + "requirement": "任务总览覆盖所有仓库任务:能够聚合所有仓库、所有节点相关任务,而不是仅显示当前仓库任务。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/tasks.vue", + "manager/backend/services/footstone/application/repository_task.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/application/repository_task_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go", + "manager/backend/services/footstone/presentation/repository_task_controller_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 5, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "aae50fd750d54a8181d33fe1b12aac83d9e60bcf80413098485ce62c18e53859" + }, + { + "ordinal": 2, + "sha256": "80002c536e6ba09a4b7ad0349162d2472fb0231b9bf6f321345fb85f094b288d" + }, + { + "ordinal": 3, + "sha256": "8685d020c0525fdca2bd4bfd7058eb7064a673bc092f2330b174786f8886c856" + }, + { + "ordinal": 4, + "sha256": "fd69decbdb6dbb1001717b7c265599342f24ebba0efd480bef1b07e95981731a" + }, + { + "ordinal": 5, + "sha256": "d4f0dfc1f0b27dc5f4c0270f82a4b5d74d524d8a13269391763fd091ecbee5e7" + } + ] + }, + { + "id": "12", + "requirement": "任务总览顶部有统计信息:至少显示等待中、执行中、失败、成功数量;点击对应统计项能够直接应用状态筛选。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/tasks.vue", + "manager/backend/services/footstone/application/repository_task.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/application/repository_task_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go", + "manager/backend/services/footstone/presentation/repository_task_controller_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 5, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "aae50fd750d54a8181d33fe1b12aac83d9e60bcf80413098485ce62c18e53859" + }, + { + "ordinal": 2, + "sha256": "80002c536e6ba09a4b7ad0349162d2472fb0231b9bf6f321345fb85f094b288d" + }, + { + "ordinal": 3, + "sha256": "8685d020c0525fdca2bd4bfd7058eb7064a673bc092f2330b174786f8886c856" + }, + { + "ordinal": 4, + "sha256": "fd69decbdb6dbb1001717b7c265599342f24ebba0efd480bef1b07e95981731a" + }, + { + "ordinal": 5, + "sha256": "d4f0dfc1f0b27dc5f4c0270f82a4b5d74d524d8a13269391763fd091ecbee5e7" + } + ] + }, + { + "id": "13", + "requirement": "任务总览支持完整筛选:至少支持任务状态、任务类型、仓库、节点、触发方式和时间范围筛选。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/tasks.vue", + "manager/backend/services/footstone/application/repository_task.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/application/repository_task_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go", + "manager/backend/services/footstone/presentation/repository_task_controller_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 5, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "aae50fd750d54a8181d33fe1b12aac83d9e60bcf80413098485ce62c18e53859" + }, + { + "ordinal": 2, + "sha256": "80002c536e6ba09a4b7ad0349162d2472fb0231b9bf6f321345fb85f094b288d" + }, + { + "ordinal": 3, + "sha256": "8685d020c0525fdca2bd4bfd7058eb7064a673bc092f2330b174786f8886c856" + }, + { + "ordinal": 4, + "sha256": "fd69decbdb6dbb1001717b7c265599342f24ebba0efd480bef1b07e95981731a" + }, + { + "ordinal": 5, + "sha256": "d4f0dfc1f0b27dc5f4c0270f82a4b5d74d524d8a13269391763fd091ecbee5e7" + } + ] + }, + { + "id": "14", + "requirement": "任务触发来源明确:至少区分人工触发、定时触发、系统触发;如果存在自动重试,也应单独标识。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/tasks.vue", + "manager/backend/services/footstone/application/repository_task.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/application/repository_task_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go", + "manager/backend/services/footstone/presentation/repository_task_controller_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 5, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "aae50fd750d54a8181d33fe1b12aac83d9e60bcf80413098485ce62c18e53859" + }, + { + "ordinal": 2, + "sha256": "80002c536e6ba09a4b7ad0349162d2472fb0231b9bf6f321345fb85f094b288d" + }, + { + "ordinal": 3, + "sha256": "8685d020c0525fdca2bd4bfd7058eb7064a673bc092f2330b174786f8886c856" + }, + { + "ordinal": 4, + "sha256": "fd69decbdb6dbb1001717b7c265599342f24ebba0efd480bef1b07e95981731a" + }, + { + "ordinal": 5, + "sha256": "d4f0dfc1f0b27dc5f4c0270f82a4b5d74d524d8a13269391763fd091ecbee5e7" + } + ] + }, + { + "id": "15", + "requirement": "任务列表按时间顺序合理排列:默认优先展示最新任务,通常按创建或开始时间倒序。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/tasks.vue", + "manager/backend/services/footstone/application/repository_task.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/application/repository_task_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go", + "manager/backend/services/footstone/presentation/repository_task_controller_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 5, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "aae50fd750d54a8181d33fe1b12aac83d9e60bcf80413098485ce62c18e53859" + }, + { + "ordinal": 2, + "sha256": "80002c536e6ba09a4b7ad0349162d2472fb0231b9bf6f321345fb85f094b288d" + }, + { + "ordinal": 3, + "sha256": "8685d020c0525fdca2bd4bfd7058eb7064a673bc092f2330b174786f8886c856" + }, + { + "ordinal": 4, + "sha256": "fd69decbdb6dbb1001717b7c265599342f24ebba0efd480bef1b07e95981731a" + }, + { + "ordinal": 5, + "sha256": "d4f0dfc1f0b27dc5f4c0270f82a4b5d74d524d8a13269391763fd091ecbee5e7" + } + ] + }, + { + "id": "16", + "requirement": "任务列表显示节点来源信息:活动任务不再缺少来源节点或执行节点信息。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/tasks.vue", + "manager/backend/services/footstone/application/repository_task.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/application/repository_task_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go", + "manager/backend/services/footstone/presentation/repository_task_controller_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 5, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "aae50fd750d54a8181d33fe1b12aac83d9e60bcf80413098485ce62c18e53859" + }, + { + "ordinal": 2, + "sha256": "80002c536e6ba09a4b7ad0349162d2472fb0231b9bf6f321345fb85f094b288d" + }, + { + "ordinal": 3, + "sha256": "8685d020c0525fdca2bd4bfd7058eb7064a673bc092f2330b174786f8886c856" + }, + { + "ordinal": 4, + "sha256": "fd69decbdb6dbb1001717b7c265599342f24ebba0efd480bef1b07e95981731a" + }, + { + "ordinal": 5, + "sha256": "d4f0dfc1f0b27dc5f4c0270f82a4b5d74d524d8a13269391763fd091ecbee5e7" + } + ] + }, + { + "id": "17", + "requirement": "失败任务一眼可见:失败状态使用项目已有错误状态视觉,不额外创建新色板。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/tasks.vue", + "manager/backend/services/footstone/application/repository_task.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/application/repository_task_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go", + "manager/backend/services/footstone/presentation/repository_task_controller_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 5, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "aae50fd750d54a8181d33fe1b12aac83d9e60bcf80413098485ce62c18e53859" + }, + { + "ordinal": 2, + "sha256": "80002c536e6ba09a4b7ad0349162d2472fb0231b9bf6f321345fb85f094b288d" + }, + { + "ordinal": 3, + "sha256": "8685d020c0525fdca2bd4bfd7058eb7064a673bc092f2330b174786f8886c856" + }, + { + "ordinal": 4, + "sha256": "fd69decbdb6dbb1001717b7c265599342f24ebba0efd480bef1b07e95981731a" + }, + { + "ordinal": 5, + "sha256": "d4f0dfc1f0b27dc5f4c0270f82a4b5d74d524d8a13269391763fd091ecbee5e7" + } + ] + }, + { + "id": "18", + "requirement": "失败原因无需多层点击:失败摘要应直接出现在任务列表或一级展开区域,用户不需要连续打开两层详情才能知道失败原因。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/tasks.vue", + "manager/backend/services/footstone/application/repository_task.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/application/repository_task_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go", + "manager/backend/services/footstone/presentation/repository_task_controller_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 5, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "aae50fd750d54a8181d33fe1b12aac83d9e60bcf80413098485ce62c18e53859" + }, + { + "ordinal": 2, + "sha256": "80002c536e6ba09a4b7ad0349162d2472fb0231b9bf6f321345fb85f094b288d" + }, + { + "ordinal": 3, + "sha256": "8685d020c0525fdca2bd4bfd7058eb7064a673bc092f2330b174786f8886c856" + }, + { + "ordinal": 4, + "sha256": "fd69decbdb6dbb1001717b7c265599342f24ebba0efd480bef1b07e95981731a" + }, + { + "ordinal": 5, + "sha256": "d4f0dfc1f0b27dc5f4c0270f82a4b5d74d524d8a13269391763fd091ecbee5e7" + } + ] + }, + { + "id": "19", + "requirement": "任务详情仍可展开查看完整信息:完整日志、错误码、上下文等可通过 Drawer 或详情区域查看。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/tasks.vue", + "manager/backend/services/footstone/application/repository_task.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/application/repository_task_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go", + "manager/backend/services/footstone/presentation/repository_task_controller_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 5, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "aae50fd750d54a8181d33fe1b12aac83d9e60bcf80413098485ce62c18e53859" + }, + { + "ordinal": 2, + "sha256": "80002c536e6ba09a4b7ad0349162d2472fb0231b9bf6f321345fb85f094b288d" + }, + { + "ordinal": 3, + "sha256": "8685d020c0525fdca2bd4bfd7058eb7064a673bc092f2330b174786f8886c856" + }, + { + "ordinal": 4, + "sha256": "fd69decbdb6dbb1001717b7c265599342f24ebba0efd480bef1b07e95981731a" + }, + { + "ordinal": 5, + "sha256": "d4f0dfc1f0b27dc5f4c0270f82a4b5d74d524d8a13269391763fd091ecbee5e7" + } + ] + }, + { + "id": "20", + "requirement": "仓库节点提升为一级页面:节点不再藏在单个仓库页面内,也不再依赖“进去再返回”的导航方式。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/nodes.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/service.go", + "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/repository.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go", + "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/statistics_test.go", + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 6, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "006c9688f5fb27ab2b0de33df0c7dc8e53aeb85d67148aee39a67a19c351496d" + }, + { + "ordinal": 2, + "sha256": "d53707d355c5a3d321198650880744aa24943f5752f79a5e7b51a8eece230ee4" + }, + { + "ordinal": 3, + "sha256": "e3f820ac9da2aa487b4a777c894b1a0bcc19f6d877d99b19451d1591ab7e6769" + }, + { + "ordinal": 4, + "sha256": "8544cdef271daa08883d14a84a5213c3bb0db5a369fcfa27733acaf8032e76ba" + }, + { + "ordinal": 5, + "sha256": "0ad7770ba011daab45c52fd31a38137c39395c0da76b6f1735f4b9a4086699e7" + }, + { + "ordinal": 6, + "sha256": "93a4f3562243b89dc2514f671e35748da386850f274e20b3a0f88996a6a750bf" + } + ] + }, + { + "id": "21", + "requirement": "节点页顶部提供集群容量概览:至少展示节点总数、在线节点数、集群总容量、已使用容量和告急节点数。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/nodes.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/service.go", + "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/repository.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go", + "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/statistics_test.go", + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 6, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "006c9688f5fb27ab2b0de33df0c7dc8e53aeb85d67148aee39a67a19c351496d" + }, + { + "ordinal": 2, + "sha256": "d53707d355c5a3d321198650880744aa24943f5752f79a5e7b51a8eece230ee4" + }, + { + "ordinal": 3, + "sha256": "e3f820ac9da2aa487b4a777c894b1a0bcc19f6d877d99b19451d1591ab7e6769" + }, + { + "ordinal": 4, + "sha256": "8544cdef271daa08883d14a84a5213c3bb0db5a369fcfa27733acaf8032e76ba" + }, + { + "ordinal": 5, + "sha256": "0ad7770ba011daab45c52fd31a38137c39395c0da76b6f1735f4b9a4086699e7" + }, + { + "ordinal": 6, + "sha256": "93a4f3562243b89dc2514f671e35748da386850f274e20b3a0f88996a6a750bf" + } + ] + }, + { + "id": "22", + "requirement": "节点列表直接展示部署仓库情况:用户必须能直接知道某台机器上部署了多少仓库,并看到仓库概要信息。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/nodes.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/service.go", + "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/repository.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go", + "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/statistics_test.go", + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 6, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "006c9688f5fb27ab2b0de33df0c7dc8e53aeb85d67148aee39a67a19c351496d" + }, + { + "ordinal": 2, + "sha256": "d53707d355c5a3d321198650880744aa24943f5752f79a5e7b51a8eece230ee4" + }, + { + "ordinal": 3, + "sha256": "e3f820ac9da2aa487b4a777c894b1a0bcc19f6d877d99b19451d1591ab7e6769" + }, + { + "ordinal": 4, + "sha256": "8544cdef271daa08883d14a84a5213c3bb0db5a369fcfa27733acaf8032e76ba" + }, + { + "ordinal": 5, + "sha256": "0ad7770ba011daab45c52fd31a38137c39395c0da76b6f1735f4b9a4086699e7" + }, + { + "ordinal": 6, + "sha256": "93a4f3562243b89dc2514f671e35748da386850f274e20b3a0f88996a6a750bf" + } + ] + }, + { + "id": "23", + "requirement": "节点详情能够看到完整仓库清单:每个仓库至少显示仓库名称、系统/版本、仓库类型、空间占用和状态。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/nodes.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/service.go", + "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/repository.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go", + "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/statistics_test.go", + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 6, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "006c9688f5fb27ab2b0de33df0c7dc8e53aeb85d67148aee39a67a19c351496d" + }, + { + "ordinal": 2, + "sha256": "d53707d355c5a3d321198650880744aa24943f5752f79a5e7b51a8eece230ee4" + }, + { + "ordinal": 3, + "sha256": "e3f820ac9da2aa487b4a777c894b1a0bcc19f6d877d99b19451d1591ab7e6769" + }, + { + "ordinal": 4, + "sha256": "8544cdef271daa08883d14a84a5213c3bb0db5a369fcfa27733acaf8032e76ba" + }, + { + "ordinal": 5, + "sha256": "0ad7770ba011daab45c52fd31a38137c39395c0da76b6f1735f4b9a4086699e7" + }, + { + "ordinal": 6, + "sha256": "93a4f3562243b89dc2514f671e35748da386850f274e20b3a0f88996a6a750bf" + } + ] + }, + { + "id": "23A", + "requirement": "节点详情必须形成以仓库节点为主视角的 Repository/Source 列表:对于当前节点承载的每个仓库,至少直接展示仓库名称、系统/产品、版本、仓库类型、架构、包数量、当前 Revision、最近同步时间、最近同步状态,以及下次定时同步时间(如已配置)。不能只显示仓库名称和简单状态。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/nodes.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/service.go", + "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/repository.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go", + "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/statistics_test.go", + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 6, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "006c9688f5fb27ab2b0de33df0c7dc8e53aeb85d67148aee39a67a19c351496d" + }, + { + "ordinal": 2, + "sha256": "d53707d355c5a3d321198650880744aa24943f5752f79a5e7b51a8eece230ee4" + }, + { + "ordinal": 3, + "sha256": "e3f820ac9da2aa487b4a777c894b1a0bcc19f6d877d99b19451d1591ab7e6769" + }, + { + "ordinal": 4, + "sha256": "8544cdef271daa08883d14a84a5213c3bb0db5a369fcfa27733acaf8032e76ba" + }, + { + "ordinal": 5, + "sha256": "0ad7770ba011daab45c52fd31a38137c39395c0da76b6f1735f4b9a4086699e7" + }, + { + "ordinal": 6, + "sha256": "93a4f3562243b89dc2514f671e35748da386850f274e20b3a0f88996a6a750bf" + } + ] + }, + { + "id": "23B", + "requirement": "节点视角必须明确区分仓库“内容大小”和“实际占用”:logical_bytes 表示当前发布 Revision 中的软件包逻辑总大小,physical_bytes 表示该仓库在当前节点上的真实磁盘占用,可包括 immutable revisions、staging、quarantine、metadata 等;这两个指标不得混用,核心统计数据必须由后端/节点真实统计提供,不能由前端临时估算。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/nodes.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/service.go", + "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/repository.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go", + "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/statistics_test.go", + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 6, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "006c9688f5fb27ab2b0de33df0c7dc8e53aeb85d67148aee39a67a19c351496d" + }, + { + "ordinal": 2, + "sha256": "d53707d355c5a3d321198650880744aa24943f5752f79a5e7b51a8eece230ee4" + }, + { + "ordinal": 3, + "sha256": "e3f820ac9da2aa487b4a777c894b1a0bcc19f6d877d99b19451d1591ab7e6769" + }, + { + "ordinal": 4, + "sha256": "8544cdef271daa08883d14a84a5213c3bb0db5a369fcfa27733acaf8032e76ba" + }, + { + "ordinal": 5, + "sha256": "0ad7770ba011daab45c52fd31a38137c39395c0da76b6f1735f4b9a4086699e7" + }, + { + "ordinal": 6, + "sha256": "93a4f3562243b89dc2514f671e35748da386850f274e20b3a0f88996a6a750bf" + } + ] + }, + { + "id": "24", + "requirement": "节点存储容量直接显示在主列表:至少展示已用容量、总容量和使用率,不能再必须打开详情才能查看。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/nodes.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/service.go", + "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/repository.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go", + "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/statistics_test.go", + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 6, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "006c9688f5fb27ab2b0de33df0c7dc8e53aeb85d67148aee39a67a19c351496d" + }, + { + "ordinal": 2, + "sha256": "d53707d355c5a3d321198650880744aa24943f5752f79a5e7b51a8eece230ee4" + }, + { + "ordinal": 3, + "sha256": "e3f820ac9da2aa487b4a777c894b1a0bcc19f6d877d99b19451d1591ab7e6769" + }, + { + "ordinal": 4, + "sha256": "8544cdef271daa08883d14a84a5213c3bb0db5a369fcfa27733acaf8032e76ba" + }, + { + "ordinal": 5, + "sha256": "0ad7770ba011daab45c52fd31a38137c39395c0da76b6f1735f4b9a4086699e7" + }, + { + "ordinal": 6, + "sha256": "93a4f3562243b89dc2514f671e35748da386850f274e20b3a0f88996a6a750bf" + } + ] + }, + { + "id": "25", + "requirement": "节点容量实现四级预警:至少分为“容量充足、容量适中、容量偏高、容量告急”四档。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/nodes.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/service.go", + "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/repository.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go", + "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/statistics_test.go", + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 6, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "006c9688f5fb27ab2b0de33df0c7dc8e53aeb85d67148aee39a67a19c351496d" + }, + { + "ordinal": 2, + "sha256": "d53707d355c5a3d321198650880744aa24943f5752f79a5e7b51a8eece230ee4" + }, + { + "ordinal": 3, + "sha256": "e3f820ac9da2aa487b4a777c894b1a0bcc19f6d877d99b19451d1591ab7e6769" + }, + { + "ordinal": 4, + "sha256": "8544cdef271daa08883d14a84a5213c3bb0db5a369fcfa27733acaf8032e76ba" + }, + { + "ordinal": 5, + "sha256": "0ad7770ba011daab45c52fd31a38137c39395c0da76b6f1735f4b9a4086699e7" + }, + { + "ordinal": 6, + "sha256": "93a4f3562243b89dc2514f671e35748da386850f274e20b3a0f88996a6a750bf" + } + ] + }, + { + "id": "26", + "requirement": "容量状态颜色递进:四档状态必须使用当前项目已有 success/info或primary/warning/danger 等 Token,不能新建一套颜色体系。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/nodes.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/service.go", + "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/repository.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go", + "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/statistics_test.go", + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 6, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "006c9688f5fb27ab2b0de33df0c7dc8e53aeb85d67148aee39a67a19c351496d" + }, + { + "ordinal": 2, + "sha256": "d53707d355c5a3d321198650880744aa24943f5752f79a5e7b51a8eece230ee4" + }, + { + "ordinal": 3, + "sha256": "e3f820ac9da2aa487b4a777c894b1a0bcc19f6d877d99b19451d1591ab7e6769" + }, + { + "ordinal": 4, + "sha256": "8544cdef271daa08883d14a84a5213c3bb0db5a369fcfa27733acaf8032e76ba" + }, + { + "ordinal": 5, + "sha256": "0ad7770ba011daab45c52fd31a38137c39395c0da76b6f1735f4b9a4086699e7" + }, + { + "ordinal": 6, + "sha256": "93a4f3562243b89dc2514f671e35748da386850f274e20b3a0f88996a6a750bf" + } + ] + }, + { + "id": "27", + "requirement": "容量阈值集中维护:不得在多个 Vue 文件中散落 Magic Number;默认可采用 <60%、60%-75%、75%-90%、>=90%,如已有产品配置则以统一配置为准。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/nodes.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/service.go", + "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/repository.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go", + "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/statistics_test.go", + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 6, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "006c9688f5fb27ab2b0de33df0c7dc8e53aeb85d67148aee39a67a19c351496d" + }, + { + "ordinal": 2, + "sha256": "d53707d355c5a3d321198650880744aa24943f5752f79a5e7b51a8eece230ee4" + }, + { + "ordinal": 3, + "sha256": "e3f820ac9da2aa487b4a777c894b1a0bcc19f6d877d99b19451d1591ab7e6769" + }, + { + "ordinal": 4, + "sha256": "8544cdef271daa08883d14a84a5213c3bb0db5a369fcfa27733acaf8032e76ba" + }, + { + "ordinal": 5, + "sha256": "0ad7770ba011daab45c52fd31a38137c39395c0da76b6f1735f4b9a4086699e7" + }, + { + "ordinal": 6, + "sha256": "93a4f3562243b89dc2514f671e35748da386850f274e20b3a0f88996a6a750bf" + } + ] + }, + { + "id": "28", + "requirement": "节点详情采用一级页面 + Drawer 方案:不再新增独立二级 Node Detail Page;节点完整详情通过右侧 Drawer 展示。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/nodes.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/service.go", + "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/repository.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go", + "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/statistics_test.go", + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 6, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "006c9688f5fb27ab2b0de33df0c7dc8e53aeb85d67148aee39a67a19c351496d" + }, + { + "ordinal": 2, + "sha256": "d53707d355c5a3d321198650880744aa24943f5752f79a5e7b51a8eece230ee4" + }, + { + "ordinal": 3, + "sha256": "e3f820ac9da2aa487b4a777c894b1a0bcc19f6d877d99b19451d1591ab7e6769" + }, + { + "ordinal": 4, + "sha256": "8544cdef271daa08883d14a84a5213c3bb0db5a369fcfa27733acaf8032e76ba" + }, + { + "ordinal": 5, + "sha256": "0ad7770ba011daab45c52fd31a38137c39395c0da76b6f1735f4b9a4086699e7" + }, + { + "ordinal": 6, + "sha256": "93a4f3562243b89dc2514f671e35748da386850f274e20b3a0f88996a6a750bf" + } + ] + }, + { + "id": "29", + "requirement": "节点离线原因直接显示:不能只显示“离线”两个字;状态附近至少显示最近一次错误摘要或错误码,并显示最近心跳时间。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/nodes.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/service.go", + "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/repository.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go", + "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/statistics_test.go", + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 6, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "006c9688f5fb27ab2b0de33df0c7dc8e53aeb85d67148aee39a67a19c351496d" + }, + { + "ordinal": 2, + "sha256": "d53707d355c5a3d321198650880744aa24943f5752f79a5e7b51a8eece230ee4" + }, + { + "ordinal": 3, + "sha256": "e3f820ac9da2aa487b4a777c894b1a0bcc19f6d877d99b19451d1591ab7e6769" + }, + { + "ordinal": 4, + "sha256": "8544cdef271daa08883d14a84a5213c3bb0db5a369fcfa27733acaf8032e76ba" + }, + { + "ordinal": 5, + "sha256": "0ad7770ba011daab45c52fd31a38137c39395c0da76b6f1735f4b9a4086699e7" + }, + { + "ordinal": 6, + "sha256": "93a4f3562243b89dc2514f671e35748da386850f274e20b3a0f88996a6a750bf" + } + ] + }, + { + "id": "30", + "requirement": "仓库概览页完成信息层级重排:不能继续十几个键值对平铺。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/frontend/src/pages/repository/components/RepositoryGPGKeys.vue", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 12, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "61f411b15602a6383e2523bf37672b3beae600913cdac3b45fe0c8680c27ccf6" + }, + { + "ordinal": 2, + "sha256": "5c19f23ae17b6b8a0b1958023125bda6fb9082c7ec16a271ab14da6b00498676" + }, + { + "ordinal": 3, + "sha256": "dd7e723f389bbafdf70b0386287b05b9dd089329c1f7f43db7301f3a234e20a9" + }, + { + "ordinal": 4, + "sha256": "9d79bb74854749a82cf4cf2e4f1daca9443922211865ecfc3b208de3c6101439" + }, + { + "ordinal": 5, + "sha256": "8a213b7b0747dff8f520425d92240d000cc2861b3fde1f0e6934fc42125ca53d" + }, + { + "ordinal": 6, + "sha256": "ad0df51cbde8319e819c139eaded0ba428d94721d6b1c6d68b2fe8163c5a7a75" + }, + { + "ordinal": 7, + "sha256": "2c985975b2cf2a40592b35f61f23ad9d14a5f067d05266e1306af2f50c65152e" + }, + { + "ordinal": 8, + "sha256": "b7073afffec386fb842354239556421e71edaa2b49bce8d7e4ed120dcabac1f4" + }, + { + "ordinal": 9, + "sha256": "d0e9d737f1bc708eb042a4b0d47f123a8ae090f979746520517ef6be4510c519" + }, + { + "ordinal": 10, + "sha256": "4d2c2b35603be06fe3817c05e70b3943b0ed718f0c384646a759ce5157e79fd1" + }, + { + "ordinal": 11, + "sha256": "15095dbd72e8ce285b20c6b86dfdb0b4166952c1d3bf23cedd5e641a3893d81a" + }, + { + "ordinal": 12, + "sha256": "1506a2a0c9d0e6df2d913f2889d4a544544b11cfda0ca0656b20ce3ca434d387" + } + ] + }, + { + "id": "31", + "requirement": "仓库概览第一层突出核心信息:当前发布版本/Revision、发布地址、主节点在线状态必须位于最明显区域。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/frontend/src/pages/repository/components/RepositoryGPGKeys.vue", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 12, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "61f411b15602a6383e2523bf37672b3beae600913cdac3b45fe0c8680c27ccf6" + }, + { + "ordinal": 2, + "sha256": "5c19f23ae17b6b8a0b1958023125bda6fb9082c7ec16a271ab14da6b00498676" + }, + { + "ordinal": 3, + "sha256": "dd7e723f389bbafdf70b0386287b05b9dd089329c1f7f43db7301f3a234e20a9" + }, + { + "ordinal": 4, + "sha256": "9d79bb74854749a82cf4cf2e4f1daca9443922211865ecfc3b208de3c6101439" + }, + { + "ordinal": 5, + "sha256": "8a213b7b0747dff8f520425d92240d000cc2861b3fde1f0e6934fc42125ca53d" + }, + { + "ordinal": 6, + "sha256": "ad0df51cbde8319e819c139eaded0ba428d94721d6b1c6d68b2fe8163c5a7a75" + }, + { + "ordinal": 7, + "sha256": "2c985975b2cf2a40592b35f61f23ad9d14a5f067d05266e1306af2f50c65152e" + }, + { + "ordinal": 8, + "sha256": "b7073afffec386fb842354239556421e71edaa2b49bce8d7e4ed120dcabac1f4" + }, + { + "ordinal": 9, + "sha256": "d0e9d737f1bc708eb042a4b0d47f123a8ae090f979746520517ef6be4510c519" + }, + { + "ordinal": 10, + "sha256": "4d2c2b35603be06fe3817c05e70b3943b0ed718f0c384646a759ce5157e79fd1" + }, + { + "ordinal": 11, + "sha256": "15095dbd72e8ce285b20c6b86dfdb0b4166952c1d3bf23cedd5e641a3893d81a" + }, + { + "ordinal": 12, + "sha256": "1506a2a0c9d0e6df2d913f2889d4a544544b11cfda0ca0656b20ce3ca434d387" + } + ] + }, + { + "id": "32", + "requirement": "仓库概览第二层展示关键指标:至少展示包数量、总体积、Revision 数量、隔离区待处理数量。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/frontend/src/pages/repository/components/RepositoryGPGKeys.vue", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 12, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "61f411b15602a6383e2523bf37672b3beae600913cdac3b45fe0c8680c27ccf6" + }, + { + "ordinal": 2, + "sha256": "5c19f23ae17b6b8a0b1958023125bda6fb9082c7ec16a271ab14da6b00498676" + }, + { + "ordinal": 3, + "sha256": "dd7e723f389bbafdf70b0386287b05b9dd089329c1f7f43db7301f3a234e20a9" + }, + { + "ordinal": 4, + "sha256": "9d79bb74854749a82cf4cf2e4f1daca9443922211865ecfc3b208de3c6101439" + }, + { + "ordinal": 5, + "sha256": "8a213b7b0747dff8f520425d92240d000cc2861b3fde1f0e6934fc42125ca53d" + }, + { + "ordinal": 6, + "sha256": "ad0df51cbde8319e819c139eaded0ba428d94721d6b1c6d68b2fe8163c5a7a75" + }, + { + "ordinal": 7, + "sha256": "2c985975b2cf2a40592b35f61f23ad9d14a5f067d05266e1306af2f50c65152e" + }, + { + "ordinal": 8, + "sha256": "b7073afffec386fb842354239556421e71edaa2b49bce8d7e4ed120dcabac1f4" + }, + { + "ordinal": 9, + "sha256": "d0e9d737f1bc708eb042a4b0d47f123a8ae090f979746520517ef6be4510c519" + }, + { + "ordinal": 10, + "sha256": "4d2c2b35603be06fe3817c05e70b3943b0ed718f0c384646a759ce5157e79fd1" + }, + { + "ordinal": 11, + "sha256": "15095dbd72e8ce285b20c6b86dfdb0b4166952c1d3bf23cedd5e641a3893d81a" + }, + { + "ordinal": 12, + "sha256": "1506a2a0c9d0e6df2d913f2889d4a544544b11cfda0ca0656b20ce3ca434d387" + } + ] + }, + { + "id": "33", + "requirement": "低频配置下沉:仓库类型、架构、GPG 策略、上游地址、同步模式等进入下方配置区域,不与发布状态等核心信息等权展示。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/frontend/src/pages/repository/components/RepositoryGPGKeys.vue", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 12, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "61f411b15602a6383e2523bf37672b3beae600913cdac3b45fe0c8680c27ccf6" + }, + { + "ordinal": 2, + "sha256": "5c19f23ae17b6b8a0b1958023125bda6fb9082c7ec16a271ab14da6b00498676" + }, + { + "ordinal": 3, + "sha256": "dd7e723f389bbafdf70b0386287b05b9dd089329c1f7f43db7301f3a234e20a9" + }, + { + "ordinal": 4, + "sha256": "9d79bb74854749a82cf4cf2e4f1daca9443922211865ecfc3b208de3c6101439" + }, + { + "ordinal": 5, + "sha256": "8a213b7b0747dff8f520425d92240d000cc2861b3fde1f0e6934fc42125ca53d" + }, + { + "ordinal": 6, + "sha256": "ad0df51cbde8319e819c139eaded0ba428d94721d6b1c6d68b2fe8163c5a7a75" + }, + { + "ordinal": 7, + "sha256": "2c985975b2cf2a40592b35f61f23ad9d14a5f067d05266e1306af2f50c65152e" + }, + { + "ordinal": 8, + "sha256": "b7073afffec386fb842354239556421e71edaa2b49bce8d7e4ed120dcabac1f4" + }, + { + "ordinal": 9, + "sha256": "d0e9d737f1bc708eb042a4b0d47f123a8ae090f979746520517ef6be4510c519" + }, + { + "ordinal": 10, + "sha256": "4d2c2b35603be06fe3817c05e70b3943b0ed718f0c384646a759ce5157e79fd1" + }, + { + "ordinal": 11, + "sha256": "15095dbd72e8ce285b20c6b86dfdb0b4166952c1d3bf23cedd5e641a3893d81a" + }, + { + "ordinal": 12, + "sha256": "1506a2a0c9d0e6df2d913f2889d4a544544b11cfda0ca0656b20ce3ca434d387" + } + ] + }, + { + "id": "34", + "requirement": "仓库详情 Tab 数量明显收敛:上传和隔离区不得继续各自占用独立 Tab。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/frontend/src/pages/repository/components/RepositoryGPGKeys.vue", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 12, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "61f411b15602a6383e2523bf37672b3beae600913cdac3b45fe0c8680c27ccf6" + }, + { + "ordinal": 2, + "sha256": "5c19f23ae17b6b8a0b1958023125bda6fb9082c7ec16a271ab14da6b00498676" + }, + { + "ordinal": 3, + "sha256": "dd7e723f389bbafdf70b0386287b05b9dd089329c1f7f43db7301f3a234e20a9" + }, + { + "ordinal": 4, + "sha256": "9d79bb74854749a82cf4cf2e4f1daca9443922211865ecfc3b208de3c6101439" + }, + { + "ordinal": 5, + "sha256": "8a213b7b0747dff8f520425d92240d000cc2861b3fde1f0e6934fc42125ca53d" + }, + { + "ordinal": 6, + "sha256": "ad0df51cbde8319e819c139eaded0ba428d94721d6b1c6d68b2fe8163c5a7a75" + }, + { + "ordinal": 7, + "sha256": "2c985975b2cf2a40592b35f61f23ad9d14a5f067d05266e1306af2f50c65152e" + }, + { + "ordinal": 8, + "sha256": "b7073afffec386fb842354239556421e71edaa2b49bce8d7e4ed120dcabac1f4" + }, + { + "ordinal": 9, + "sha256": "d0e9d737f1bc708eb042a4b0d47f123a8ae090f979746520517ef6be4510c519" + }, + { + "ordinal": 10, + "sha256": "4d2c2b35603be06fe3817c05e70b3943b0ed718f0c384646a759ce5157e79fd1" + }, + { + "ordinal": 11, + "sha256": "15095dbd72e8ce285b20c6b86dfdb0b4166952c1d3bf23cedd5e641a3893d81a" + }, + { + "ordinal": 12, + "sha256": "1506a2a0c9d0e6df2d913f2889d4a544544b11cfda0ca0656b20ce3ca434d387" + } + ] + }, + { + "id": "35", + "requirement": "上传功能移动到软件包页面:通过“上传软件包”按钮打开 Dialog 或 Drawer 操作。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/frontend/src/pages/repository/components/RepositoryGPGKeys.vue", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 12, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "61f411b15602a6383e2523bf37672b3beae600913cdac3b45fe0c8680c27ccf6" + }, + { + "ordinal": 2, + "sha256": "5c19f23ae17b6b8a0b1958023125bda6fb9082c7ec16a271ab14da6b00498676" + }, + { + "ordinal": 3, + "sha256": "dd7e723f389bbafdf70b0386287b05b9dd089329c1f7f43db7301f3a234e20a9" + }, + { + "ordinal": 4, + "sha256": "9d79bb74854749a82cf4cf2e4f1daca9443922211865ecfc3b208de3c6101439" + }, + { + "ordinal": 5, + "sha256": "8a213b7b0747dff8f520425d92240d000cc2861b3fde1f0e6934fc42125ca53d" + }, + { + "ordinal": 6, + "sha256": "ad0df51cbde8319e819c139eaded0ba428d94721d6b1c6d68b2fe8163c5a7a75" + }, + { + "ordinal": 7, + "sha256": "2c985975b2cf2a40592b35f61f23ad9d14a5f067d05266e1306af2f50c65152e" + }, + { + "ordinal": 8, + "sha256": "b7073afffec386fb842354239556421e71edaa2b49bce8d7e4ed120dcabac1f4" + }, + { + "ordinal": 9, + "sha256": "d0e9d737f1bc708eb042a4b0d47f123a8ae090f979746520517ef6be4510c519" + }, + { + "ordinal": 10, + "sha256": "4d2c2b35603be06fe3817c05e70b3943b0ed718f0c384646a759ce5157e79fd1" + }, + { + "ordinal": 11, + "sha256": "15095dbd72e8ce285b20c6b86dfdb0b4166952c1d3bf23cedd5e641a3893d81a" + }, + { + "ordinal": 12, + "sha256": "1506a2a0c9d0e6df2d913f2889d4a544544b11cfda0ca0656b20ce3ca434d387" + } + ] + }, + { + "id": "36", + "requirement": "隔离区移动到软件包页面:通过按钮进入;按钮必须带待处理数量 Badge,有待处理软件包时可以一眼发现。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/frontend/src/pages/repository/components/RepositoryGPGKeys.vue", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 12, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "61f411b15602a6383e2523bf37672b3beae600913cdac3b45fe0c8680c27ccf6" + }, + { + "ordinal": 2, + "sha256": "5c19f23ae17b6b8a0b1958023125bda6fb9082c7ec16a271ab14da6b00498676" + }, + { + "ordinal": 3, + "sha256": "dd7e723f389bbafdf70b0386287b05b9dd089329c1f7f43db7301f3a234e20a9" + }, + { + "ordinal": 4, + "sha256": "9d79bb74854749a82cf4cf2e4f1daca9443922211865ecfc3b208de3c6101439" + }, + { + "ordinal": 5, + "sha256": "8a213b7b0747dff8f520425d92240d000cc2861b3fde1f0e6934fc42125ca53d" + }, + { + "ordinal": 6, + "sha256": "ad0df51cbde8319e819c139eaded0ba428d94721d6b1c6d68b2fe8163c5a7a75" + }, + { + "ordinal": 7, + "sha256": "2c985975b2cf2a40592b35f61f23ad9d14a5f067d05266e1306af2f50c65152e" + }, + { + "ordinal": 8, + "sha256": "b7073afffec386fb842354239556421e71edaa2b49bce8d7e4ed120dcabac1f4" + }, + { + "ordinal": 9, + "sha256": "d0e9d737f1bc708eb042a4b0d47f123a8ae090f979746520517ef6be4510c519" + }, + { + "ordinal": 10, + "sha256": "4d2c2b35603be06fe3817c05e70b3943b0ed718f0c384646a759ce5157e79fd1" + }, + { + "ordinal": 11, + "sha256": "15095dbd72e8ce285b20c6b86dfdb0b4166952c1d3bf23cedd5e641a3893d81a" + }, + { + "ordinal": 12, + "sha256": "1506a2a0c9d0e6df2d913f2889d4a544544b11cfda0ca0656b20ce3ca434d387" + } + ] + }, + { + "id": "37", + "requirement": "GPG 配置和 Repo 配置合理合并:不再分别占据两个低频一级 Tab,可统一归入配置区域。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/frontend/src/pages/repository/components/RepositoryGPGKeys.vue", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 12, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "61f411b15602a6383e2523bf37672b3beae600913cdac3b45fe0c8680c27ccf6" + }, + { + "ordinal": 2, + "sha256": "5c19f23ae17b6b8a0b1958023125bda6fb9082c7ec16a271ab14da6b00498676" + }, + { + "ordinal": 3, + "sha256": "dd7e723f389bbafdf70b0386287b05b9dd089329c1f7f43db7301f3a234e20a9" + }, + { + "ordinal": 4, + "sha256": "9d79bb74854749a82cf4cf2e4f1daca9443922211865ecfc3b208de3c6101439" + }, + { + "ordinal": 5, + "sha256": "8a213b7b0747dff8f520425d92240d000cc2861b3fde1f0e6934fc42125ca53d" + }, + { + "ordinal": 6, + "sha256": "ad0df51cbde8319e819c139eaded0ba428d94721d6b1c6d68b2fe8163c5a7a75" + }, + { + "ordinal": 7, + "sha256": "2c985975b2cf2a40592b35f61f23ad9d14a5f067d05266e1306af2f50c65152e" + }, + { + "ordinal": 8, + "sha256": "b7073afffec386fb842354239556421e71edaa2b49bce8d7e4ed120dcabac1f4" + }, + { + "ordinal": 9, + "sha256": "d0e9d737f1bc708eb042a4b0d47f123a8ae090f979746520517ef6be4510c519" + }, + { + "ordinal": 10, + "sha256": "4d2c2b35603be06fe3817c05e70b3943b0ed718f0c384646a759ce5157e79fd1" + }, + { + "ordinal": 11, + "sha256": "15095dbd72e8ce285b20c6b86dfdb0b4166952c1d3bf23cedd5e641a3893d81a" + }, + { + "ordinal": 12, + "sha256": "1506a2a0c9d0e6df2d913f2889d4a544544b11cfda0ca0656b20ce3ca434d387" + } + ] + }, + { + "id": "38", + "requirement": "软件包列表字段增强:至少包含包名、Version、Release、Architecture、Size、签名状态、所属 Revision、入库时间、SHA256。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/frontend/src/pages/repository/components/RepositoryGPGKeys.vue", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 12, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "61f411b15602a6383e2523bf37672b3beae600913cdac3b45fe0c8680c27ccf6" + }, + { + "ordinal": 2, + "sha256": "5c19f23ae17b6b8a0b1958023125bda6fb9082c7ec16a271ab14da6b00498676" + }, + { + "ordinal": 3, + "sha256": "dd7e723f389bbafdf70b0386287b05b9dd089329c1f7f43db7301f3a234e20a9" + }, + { + "ordinal": 4, + "sha256": "9d79bb74854749a82cf4cf2e4f1daca9443922211865ecfc3b208de3c6101439" + }, + { + "ordinal": 5, + "sha256": "8a213b7b0747dff8f520425d92240d000cc2861b3fde1f0e6934fc42125ca53d" + }, + { + "ordinal": 6, + "sha256": "ad0df51cbde8319e819c139eaded0ba428d94721d6b1c6d68b2fe8163c5a7a75" + }, + { + "ordinal": 7, + "sha256": "2c985975b2cf2a40592b35f61f23ad9d14a5f067d05266e1306af2f50c65152e" + }, + { + "ordinal": 8, + "sha256": "b7073afffec386fb842354239556421e71edaa2b49bce8d7e4ed120dcabac1f4" + }, + { + "ordinal": 9, + "sha256": "d0e9d737f1bc708eb042a4b0d47f123a8ae090f979746520517ef6be4510c519" + }, + { + "ordinal": 10, + "sha256": "4d2c2b35603be06fe3817c05e70b3943b0ed718f0c384646a759ce5157e79fd1" + }, + { + "ordinal": 11, + "sha256": "15095dbd72e8ce285b20c6b86dfdb0b4166952c1d3bf23cedd5e641a3893d81a" + }, + { + "ordinal": 12, + "sha256": "1506a2a0c9d0e6df2d913f2889d4a544544b11cfda0ca0656b20ce3ca434d387" + } + ] + }, + { + "id": "39", + "requirement": "软件包列表支持架构筛选。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/frontend/src/pages/repository/components/RepositoryGPGKeys.vue", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 12, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "61f411b15602a6383e2523bf37672b3beae600913cdac3b45fe0c8680c27ccf6" + }, + { + "ordinal": 2, + "sha256": "5c19f23ae17b6b8a0b1958023125bda6fb9082c7ec16a271ab14da6b00498676" + }, + { + "ordinal": 3, + "sha256": "dd7e723f389bbafdf70b0386287b05b9dd089329c1f7f43db7301f3a234e20a9" + }, + { + "ordinal": 4, + "sha256": "9d79bb74854749a82cf4cf2e4f1daca9443922211865ecfc3b208de3c6101439" + }, + { + "ordinal": 5, + "sha256": "8a213b7b0747dff8f520425d92240d000cc2861b3fde1f0e6934fc42125ca53d" + }, + { + "ordinal": 6, + "sha256": "ad0df51cbde8319e819c139eaded0ba428d94721d6b1c6d68b2fe8163c5a7a75" + }, + { + "ordinal": 7, + "sha256": "2c985975b2cf2a40592b35f61f23ad9d14a5f067d05266e1306af2f50c65152e" + }, + { + "ordinal": 8, + "sha256": "b7073afffec386fb842354239556421e71edaa2b49bce8d7e4ed120dcabac1f4" + }, + { + "ordinal": 9, + "sha256": "d0e9d737f1bc708eb042a4b0d47f123a8ae090f979746520517ef6be4510c519" + }, + { + "ordinal": 10, + "sha256": "4d2c2b35603be06fe3817c05e70b3943b0ed718f0c384646a759ce5157e79fd1" + }, + { + "ordinal": 11, + "sha256": "15095dbd72e8ce285b20c6b86dfdb0b4166952c1d3bf23cedd5e641a3893d81a" + }, + { + "ordinal": 12, + "sha256": "1506a2a0c9d0e6df2d913f2889d4a544544b11cfda0ca0656b20ce3ca434d387" + } + ] + }, + { + "id": "40", + "requirement": "软件包列表支持签名状态筛选。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/frontend/src/pages/repository/components/RepositoryGPGKeys.vue", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 12, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "61f411b15602a6383e2523bf37672b3beae600913cdac3b45fe0c8680c27ccf6" + }, + { + "ordinal": 2, + "sha256": "5c19f23ae17b6b8a0b1958023125bda6fb9082c7ec16a271ab14da6b00498676" + }, + { + "ordinal": 3, + "sha256": "dd7e723f389bbafdf70b0386287b05b9dd089329c1f7f43db7301f3a234e20a9" + }, + { + "ordinal": 4, + "sha256": "9d79bb74854749a82cf4cf2e4f1daca9443922211865ecfc3b208de3c6101439" + }, + { + "ordinal": 5, + "sha256": "8a213b7b0747dff8f520425d92240d000cc2861b3fde1f0e6934fc42125ca53d" + }, + { + "ordinal": 6, + "sha256": "ad0df51cbde8319e819c139eaded0ba428d94721d6b1c6d68b2fe8163c5a7a75" + }, + { + "ordinal": 7, + "sha256": "2c985975b2cf2a40592b35f61f23ad9d14a5f067d05266e1306af2f50c65152e" + }, + { + "ordinal": 8, + "sha256": "b7073afffec386fb842354239556421e71edaa2b49bce8d7e4ed120dcabac1f4" + }, + { + "ordinal": 9, + "sha256": "d0e9d737f1bc708eb042a4b0d47f123a8ae090f979746520517ef6be4510c519" + }, + { + "ordinal": 10, + "sha256": "4d2c2b35603be06fe3817c05e70b3943b0ed718f0c384646a759ce5157e79fd1" + }, + { + "ordinal": 11, + "sha256": "15095dbd72e8ce285b20c6b86dfdb0b4166952c1d3bf23cedd5e641a3893d81a" + }, + { + "ordinal": 12, + "sha256": "1506a2a0c9d0e6df2d913f2889d4a544544b11cfda0ca0656b20ce3ca434d387" + } + ] + }, + { + "id": "41", + "requirement": "软件包列表提供“只看未签名”快捷筛选。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/frontend/src/pages/repository/components/RepositoryGPGKeys.vue", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 12, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "61f411b15602a6383e2523bf37672b3beae600913cdac3b45fe0c8680c27ccf6" + }, + { + "ordinal": 2, + "sha256": "5c19f23ae17b6b8a0b1958023125bda6fb9082c7ec16a271ab14da6b00498676" + }, + { + "ordinal": 3, + "sha256": "dd7e723f389bbafdf70b0386287b05b9dd089329c1f7f43db7301f3a234e20a9" + }, + { + "ordinal": 4, + "sha256": "9d79bb74854749a82cf4cf2e4f1daca9443922211865ecfc3b208de3c6101439" + }, + { + "ordinal": 5, + "sha256": "8a213b7b0747dff8f520425d92240d000cc2861b3fde1f0e6934fc42125ca53d" + }, + { + "ordinal": 6, + "sha256": "ad0df51cbde8319e819c139eaded0ba428d94721d6b1c6d68b2fe8163c5a7a75" + }, + { + "ordinal": 7, + "sha256": "2c985975b2cf2a40592b35f61f23ad9d14a5f067d05266e1306af2f50c65152e" + }, + { + "ordinal": 8, + "sha256": "b7073afffec386fb842354239556421e71edaa2b49bce8d7e4ed120dcabac1f4" + }, + { + "ordinal": 9, + "sha256": "d0e9d737f1bc708eb042a4b0d47f123a8ae090f979746520517ef6be4510c519" + }, + { + "ordinal": 10, + "sha256": "4d2c2b35603be06fe3817c05e70b3943b0ed718f0c384646a759ce5157e79fd1" + }, + { + "ordinal": 11, + "sha256": "15095dbd72e8ce285b20c6b86dfdb0b4166952c1d3bf23cedd5e641a3893d81a" + }, + { + "ordinal": 12, + "sha256": "1506a2a0c9d0e6df2d913f2889d4a544544b11cfda0ca0656b20ce3ca434d387" + } + ] + }, + { + "id": "42", + "requirement": "Repo 配置进入页面后自动生成:用户不再需要额外点击 Generate。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/frontend/src/pages/repository/components/RepositoryGPGKeys.vue", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 12, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "61f411b15602a6383e2523bf37672b3beae600913cdac3b45fe0c8680c27ccf6" + }, + { + "ordinal": 2, + "sha256": "5c19f23ae17b6b8a0b1958023125bda6fb9082c7ec16a271ab14da6b00498676" + }, + { + "ordinal": 3, + "sha256": "dd7e723f389bbafdf70b0386287b05b9dd089329c1f7f43db7301f3a234e20a9" + }, + { + "ordinal": 4, + "sha256": "9d79bb74854749a82cf4cf2e4f1daca9443922211865ecfc3b208de3c6101439" + }, + { + "ordinal": 5, + "sha256": "8a213b7b0747dff8f520425d92240d000cc2861b3fde1f0e6934fc42125ca53d" + }, + { + "ordinal": 6, + "sha256": "ad0df51cbde8319e819c139eaded0ba428d94721d6b1c6d68b2fe8163c5a7a75" + }, + { + "ordinal": 7, + "sha256": "2c985975b2cf2a40592b35f61f23ad9d14a5f067d05266e1306af2f50c65152e" + }, + { + "ordinal": 8, + "sha256": "b7073afffec386fb842354239556421e71edaa2b49bce8d7e4ed120dcabac1f4" + }, + { + "ordinal": 9, + "sha256": "d0e9d737f1bc708eb042a4b0d47f123a8ae090f979746520517ef6be4510c519" + }, + { + "ordinal": 10, + "sha256": "4d2c2b35603be06fe3817c05e70b3943b0ed718f0c384646a759ce5157e79fd1" + }, + { + "ordinal": 11, + "sha256": "15095dbd72e8ce285b20c6b86dfdb0b4166952c1d3bf23cedd5e641a3893d81a" + }, + { + "ordinal": 12, + "sha256": "1506a2a0c9d0e6df2d913f2889d4a544544b11cfda0ca0656b20ce3ca434d387" + } + ] + }, + { + "id": "43", + "requirement": "current 与 immutable revision 切换时自动刷新 Repo 配置。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/frontend/src/pages/repository/components/RepositoryGPGKeys.vue", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 12, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "61f411b15602a6383e2523bf37672b3beae600913cdac3b45fe0c8680c27ccf6" + }, + { + "ordinal": 2, + "sha256": "5c19f23ae17b6b8a0b1958023125bda6fb9082c7ec16a271ab14da6b00498676" + }, + { + "ordinal": 3, + "sha256": "dd7e723f389bbafdf70b0386287b05b9dd089329c1f7f43db7301f3a234e20a9" + }, + { + "ordinal": 4, + "sha256": "9d79bb74854749a82cf4cf2e4f1daca9443922211865ecfc3b208de3c6101439" + }, + { + "ordinal": 5, + "sha256": "8a213b7b0747dff8f520425d92240d000cc2861b3fde1f0e6934fc42125ca53d" + }, + { + "ordinal": 6, + "sha256": "ad0df51cbde8319e819c139eaded0ba428d94721d6b1c6d68b2fe8163c5a7a75" + }, + { + "ordinal": 7, + "sha256": "2c985975b2cf2a40592b35f61f23ad9d14a5f067d05266e1306af2f50c65152e" + }, + { + "ordinal": 8, + "sha256": "b7073afffec386fb842354239556421e71edaa2b49bce8d7e4ed120dcabac1f4" + }, + { + "ordinal": 9, + "sha256": "d0e9d737f1bc708eb042a4b0d47f123a8ae090f979746520517ef6be4510c519" + }, + { + "ordinal": 10, + "sha256": "4d2c2b35603be06fe3817c05e70b3943b0ed718f0c384646a759ce5157e79fd1" + }, + { + "ordinal": 11, + "sha256": "15095dbd72e8ce285b20c6b86dfdb0b4166952c1d3bf23cedd5e641a3893d81a" + }, + { + "ordinal": 12, + "sha256": "1506a2a0c9d0e6df2d913f2889d4a544544b11cfda0ca0656b20ce3ca434d387" + } + ] + }, + { + "id": "44", + "requirement": "Generate 按钮移除:保留刷新、复制、下载等实际需要操作。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/frontend/src/pages/repository/components/RepositoryGPGKeys.vue", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 12, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "61f411b15602a6383e2523bf37672b3beae600913cdac3b45fe0c8680c27ccf6" + }, + { + "ordinal": 2, + "sha256": "5c19f23ae17b6b8a0b1958023125bda6fb9082c7ec16a271ab14da6b00498676" + }, + { + "ordinal": 3, + "sha256": "dd7e723f389bbafdf70b0386287b05b9dd089329c1f7f43db7301f3a234e20a9" + }, + { + "ordinal": 4, + "sha256": "9d79bb74854749a82cf4cf2e4f1daca9443922211865ecfc3b208de3c6101439" + }, + { + "ordinal": 5, + "sha256": "8a213b7b0747dff8f520425d92240d000cc2861b3fde1f0e6934fc42125ca53d" + }, + { + "ordinal": 6, + "sha256": "ad0df51cbde8319e819c139eaded0ba428d94721d6b1c6d68b2fe8163c5a7a75" + }, + { + "ordinal": 7, + "sha256": "2c985975b2cf2a40592b35f61f23ad9d14a5f067d05266e1306af2f50c65152e" + }, + { + "ordinal": 8, + "sha256": "b7073afffec386fb842354239556421e71edaa2b49bce8d7e4ed120dcabac1f4" + }, + { + "ordinal": 9, + "sha256": "d0e9d737f1bc708eb042a4b0d47f123a8ae090f979746520517ef6be4510c519" + }, + { + "ordinal": 10, + "sha256": "4d2c2b35603be06fe3817c05e70b3943b0ed718f0c384646a759ce5157e79fd1" + }, + { + "ordinal": 11, + "sha256": "15095dbd72e8ce285b20c6b86dfdb0b4166952c1d3bf23cedd5e641a3893d81a" + }, + { + "ordinal": 12, + "sha256": "1506a2a0c9d0e6df2d913f2889d4a544544b11cfda0ca0656b20ce3ca434d387" + } + ] + }, + { + "id": "45", + "requirement": "支持按系统/产品 + 版本批量生成 Repo 配置:例如选择 OpenCloudOS 9.6 后能够一次得到 BaseOS、Updates、Private 等多个仓库的配置。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/frontend/src/pages/repository/components/RepositoryGPGKeys.vue", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 12, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "61f411b15602a6383e2523bf37672b3beae600913cdac3b45fe0c8680c27ccf6" + }, + { + "ordinal": 2, + "sha256": "5c19f23ae17b6b8a0b1958023125bda6fb9082c7ec16a271ab14da6b00498676" + }, + { + "ordinal": 3, + "sha256": "dd7e723f389bbafdf70b0386287b05b9dd089329c1f7f43db7301f3a234e20a9" + }, + { + "ordinal": 4, + "sha256": "9d79bb74854749a82cf4cf2e4f1daca9443922211865ecfc3b208de3c6101439" + }, + { + "ordinal": 5, + "sha256": "8a213b7b0747dff8f520425d92240d000cc2861b3fde1f0e6934fc42125ca53d" + }, + { + "ordinal": 6, + "sha256": "ad0df51cbde8319e819c139eaded0ba428d94721d6b1c6d68b2fe8163c5a7a75" + }, + { + "ordinal": 7, + "sha256": "2c985975b2cf2a40592b35f61f23ad9d14a5f067d05266e1306af2f50c65152e" + }, + { + "ordinal": 8, + "sha256": "b7073afffec386fb842354239556421e71edaa2b49bce8d7e4ed120dcabac1f4" + }, + { + "ordinal": 9, + "sha256": "d0e9d737f1bc708eb042a4b0d47f123a8ae090f979746520517ef6be4510c519" + }, + { + "ordinal": 10, + "sha256": "4d2c2b35603be06fe3817c05e70b3943b0ed718f0c384646a759ce5157e79fd1" + }, + { + "ordinal": 11, + "sha256": "15095dbd72e8ce285b20c6b86dfdb0b4166952c1d3bf23cedd5e641a3893d81a" + }, + { + "ordinal": 12, + "sha256": "1506a2a0c9d0e6df2d913f2889d4a544544b11cfda0ca0656b20ce3ca434d387" + } + ] + }, + { + "id": "46", + "requirement": "批量 Repo 配置可以整体复制:不要求用户分别进入十几个仓库逐个复制。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/frontend/src/pages/repository/components/RepositoryGPGKeys.vue", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 12, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "61f411b15602a6383e2523bf37672b3beae600913cdac3b45fe0c8680c27ccf6" + }, + { + "ordinal": 2, + "sha256": "5c19f23ae17b6b8a0b1958023125bda6fb9082c7ec16a271ab14da6b00498676" + }, + { + "ordinal": 3, + "sha256": "dd7e723f389bbafdf70b0386287b05b9dd089329c1f7f43db7301f3a234e20a9" + }, + { + "ordinal": 4, + "sha256": "9d79bb74854749a82cf4cf2e4f1daca9443922211865ecfc3b208de3c6101439" + }, + { + "ordinal": 5, + "sha256": "8a213b7b0747dff8f520425d92240d000cc2861b3fde1f0e6934fc42125ca53d" + }, + { + "ordinal": 6, + "sha256": "ad0df51cbde8319e819c139eaded0ba428d94721d6b1c6d68b2fe8163c5a7a75" + }, + { + "ordinal": 7, + "sha256": "2c985975b2cf2a40592b35f61f23ad9d14a5f067d05266e1306af2f50c65152e" + }, + { + "ordinal": 8, + "sha256": "b7073afffec386fb842354239556421e71edaa2b49bce8d7e4ed120dcabac1f4" + }, + { + "ordinal": 9, + "sha256": "d0e9d737f1bc708eb042a4b0d47f123a8ae090f979746520517ef6be4510c519" + }, + { + "ordinal": 10, + "sha256": "4d2c2b35603be06fe3817c05e70b3943b0ed718f0c384646a759ce5157e79fd1" + }, + { + "ordinal": 11, + "sha256": "15095dbd72e8ce285b20c6b86dfdb0b4166952c1d3bf23cedd5e641a3893d81a" + }, + { + "ordinal": 12, + "sha256": "1506a2a0c9d0e6df2d913f2889d4a544544b11cfda0ca0656b20ce3ca434d387" + } + ] + }, + { + "id": "47", + "requirement": "原有发布 URL 保持兼容:不得因为新增更友好的系统/版本分类 URL 而破坏已有 /repositories/{publishKey}/... 地址。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/frontend/src/pages/repository/components/RepositoryGPGKeys.vue", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 12, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "61f411b15602a6383e2523bf37672b3beae600913cdac3b45fe0c8680c27ccf6" + }, + { + "ordinal": 2, + "sha256": "5c19f23ae17b6b8a0b1958023125bda6fb9082c7ec16a271ab14da6b00498676" + }, + { + "ordinal": 3, + "sha256": "dd7e723f389bbafdf70b0386287b05b9dd089329c1f7f43db7301f3a234e20a9" + }, + { + "ordinal": 4, + "sha256": "9d79bb74854749a82cf4cf2e4f1daca9443922211865ecfc3b208de3c6101439" + }, + { + "ordinal": 5, + "sha256": "8a213b7b0747dff8f520425d92240d000cc2861b3fde1f0e6934fc42125ca53d" + }, + { + "ordinal": 6, + "sha256": "ad0df51cbde8319e819c139eaded0ba428d94721d6b1c6d68b2fe8163c5a7a75" + }, + { + "ordinal": 7, + "sha256": "2c985975b2cf2a40592b35f61f23ad9d14a5f067d05266e1306af2f50c65152e" + }, + { + "ordinal": 8, + "sha256": "b7073afffec386fb842354239556421e71edaa2b49bce8d7e4ed120dcabac1f4" + }, + { + "ordinal": 9, + "sha256": "d0e9d737f1bc708eb042a4b0d47f123a8ae090f979746520517ef6be4510c519" + }, + { + "ordinal": 10, + "sha256": "4d2c2b35603be06fe3817c05e70b3943b0ed718f0c384646a759ce5157e79fd1" + }, + { + "ordinal": 11, + "sha256": "15095dbd72e8ce285b20c6b86dfdb0b4166952c1d3bf23cedd5e641a3893d81a" + }, + { + "ordinal": 12, + "sha256": "1506a2a0c9d0e6df2d913f2889d4a544544b11cfda0ca0656b20ce3ca434d387" + } + ] + }, + { + "id": "48", + "requirement": "仓库节点服务完成 RPM 打包:RPM 至少包含节点程序、systemd unit、默认配置及必要目录创建逻辑。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryNodeInstallManager.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle.go", + "manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm_test.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_test.go", + "manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 4, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "c846b07ab2d78c2f59908cd3f282119ef155382be11be59fc1be54261a51ad99" + }, + { + "ordinal": 2, + "sha256": "f0b3809aa1831291d63c90863d97a4ecf377dc9aca28f8d11ad3f45524472b17" + }, + { + "ordinal": 3, + "sha256": "5ef4baffb490d42a95836cec0c6f2fa1c07a2f5580f9d67e193dc57dd2f7d15c" + }, + { + "ordinal": 4, + "sha256": "1204c6fa91f42043f9fb2e45a8ef82fd306295085fb7fc21f587887df5dfdac1" + } + ] + }, + { + "id": "49", + "requirement": "RPM 中不得包含节点私钥和长期注册凭据。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryNodeInstallManager.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle.go", + "manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm_test.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_test.go", + "manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 4, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "c846b07ab2d78c2f59908cd3f282119ef155382be11be59fc1be54261a51ad99" + }, + { + "ordinal": 2, + "sha256": "f0b3809aa1831291d63c90863d97a4ecf377dc9aca28f8d11ad3f45524472b17" + }, + { + "ordinal": 3, + "sha256": "5ef4baffb490d42a95836cec0c6f2fa1c07a2f5580f9d67e193dc57dd2f7d15c" + }, + { + "ordinal": 4, + "sha256": "1204c6fa91f42043f9fb2e45a8ef82fd306295085fb7fc21f587887df5dfdac1" + } + ] + }, + { + "id": "50", + "requirement": "节点页面支持下载安装 RPM:至少能够获得 RPM 下载入口或安装地址。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryNodeInstallManager.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle.go", + "manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm_test.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_test.go", + "manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 4, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "c846b07ab2d78c2f59908cd3f282119ef155382be11be59fc1be54261a51ad99" + }, + { + "ordinal": 2, + "sha256": "f0b3809aa1831291d63c90863d97a4ecf377dc9aca28f8d11ad3f45524472b17" + }, + { + "ordinal": 3, + "sha256": "5ef4baffb490d42a95836cec0c6f2fa1c07a2f5580f9d67e193dc57dd2f7d15c" + }, + { + "ordinal": 4, + "sha256": "1204c6fa91f42043f9fb2e45a8ef82fd306295085fb7fc21f587887df5dfdac1" + } + ] + }, + { + "id": "51", + "requirement": "节点支持通过 TMS 自动安装 RPM:用户可选择目标机器,通过 TMS 执行下载、安装和服务启动。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryNodeInstallManager.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle.go", + "manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm_test.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_test.go", + "manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 4, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "c846b07ab2d78c2f59908cd3f282119ef155382be11be59fc1be54261a51ad99" + }, + { + "ordinal": 2, + "sha256": "f0b3809aa1831291d63c90863d97a4ecf377dc9aca28f8d11ad3f45524472b17" + }, + { + "ordinal": 3, + "sha256": "5ef4baffb490d42a95836cec0c6f2fa1c07a2f5580f9d67e193dc57dd2f7d15c" + }, + { + "ordinal": 4, + "sha256": "1204c6fa91f42043f9fb2e45a8ef82fd306295085fb7fc21f587887df5dfdac1" + } + ] + }, + { + "id": "52", + "requirement": "TMS 安装完成后节点自动注册到控制台:用户不再需要手工复制证书指纹、粘贴指纹或人工创建 Node。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryNodeInstallManager.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle.go", + "manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm_test.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_test.go", + "manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 4, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "c846b07ab2d78c2f59908cd3f282119ef155382be11be59fc1be54261a51ad99" + }, + { + "ordinal": 2, + "sha256": "f0b3809aa1831291d63c90863d97a4ecf377dc9aca28f8d11ad3f45524472b17" + }, + { + "ordinal": 3, + "sha256": "5ef4baffb490d42a95836cec0c6f2fa1c07a2f5580f9d67e193dc57dd2f7d15c" + }, + { + "ordinal": 4, + "sha256": "1204c6fa91f42043f9fb2e45a8ef82fd306295085fb7fc21f587887df5dfdac1" + } + ] + }, + { + "id": "53", + "requirement": "节点注册凭据必须一次性使用:Bootstrap Credential 使用成功后立即失效,不得重复注册。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryNodeInstallManager.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle.go", + "manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm_test.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_test.go", + "manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 4, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "c846b07ab2d78c2f59908cd3f282119ef155382be11be59fc1be54261a51ad99" + }, + { + "ordinal": 2, + "sha256": "f0b3809aa1831291d63c90863d97a4ecf377dc9aca28f8d11ad3f45524472b17" + }, + { + "ordinal": 3, + "sha256": "5ef4baffb490d42a95836cec0c6f2fa1c07a2f5580f9d67e193dc57dd2f7d15c" + }, + { + "ordinal": 4, + "sha256": "1204c6fa91f42043f9fb2e45a8ef82fd306295085fb7fc21f587887df5dfdac1" + } + ] + }, + { + "id": "54", + "requirement": "节点注册凭据必须短时有效:具有明确 expires_at 或 TTL,不允许使用永久注册 Token。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryNodeInstallManager.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle.go", + "manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm_test.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_test.go", + "manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 4, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "c846b07ab2d78c2f59908cd3f282119ef155382be11be59fc1be54261a51ad99" + }, + { + "ordinal": 2, + "sha256": "f0b3809aa1831291d63c90863d97a4ecf377dc9aca28f8d11ad3f45524472b17" + }, + { + "ordinal": 3, + "sha256": "5ef4baffb490d42a95836cec0c6f2fa1c07a2f5580f9d67e193dc57dd2f7d15c" + }, + { + "ordinal": 4, + "sha256": "1204c6fa91f42043f9fb2e45a8ef82fd306295085fb7fc21f587887df5dfdac1" + } + ] + }, + { + "id": "55", + "requirement": "节点注册凭据不能形成公共开放注册口:最好与安装任务、TMS Target 或目标节点身份绑定,并具备防 Replay 能力。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryNodeInstallManager.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle.go", + "manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm_test.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_test.go", + "manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 4, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "c846b07ab2d78c2f59908cd3f282119ef155382be11be59fc1be54261a51ad99" + }, + { + "ordinal": 2, + "sha256": "f0b3809aa1831291d63c90863d97a4ecf377dc9aca28f8d11ad3f45524472b17" + }, + { + "ordinal": 3, + "sha256": "5ef4baffb490d42a95836cec0c6f2fa1c07a2f5580f9d67e193dc57dd2f7d15c" + }, + { + "ordinal": 4, + "sha256": "1204c6fa91f42043f9fb2e45a8ef82fd306295085fb7fc21f587887df5dfdac1" + } + ] + }, + { + "id": "56", + "requirement": "节点私钥只能在节点本地生成:Manager 不得生成、保存或通过 TMS 下发 Node Private Key。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryNodeInstallManager.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle.go", + "manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm_test.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_test.go", + "manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 4, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "c846b07ab2d78c2f59908cd3f282119ef155382be11be59fc1be54261a51ad99" + }, + { + "ordinal": 2, + "sha256": "f0b3809aa1831291d63c90863d97a4ecf377dc9aca28f8d11ad3f45524472b17" + }, + { + "ordinal": 3, + "sha256": "5ef4baffb490d42a95836cec0c6f2fa1c07a2f5580f9d67e193dc57dd2f7d15c" + }, + { + "ordinal": 4, + "sha256": "1204c6fa91f42043f9fb2e45a8ef82fd306295085fb7fc21f587887df5dfdac1" + } + ] + }, + { + "id": "57", + "requirement": "节点本地私钥权限安全:必须使用安全权限保存,例如仅 root 可读,并不得进入普通任务日志。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryNodeInstallManager.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle.go", + "manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm_test.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_test.go", + "manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 4, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "c846b07ab2d78c2f59908cd3f282119ef155382be11be59fc1be54261a51ad99" + }, + { + "ordinal": 2, + "sha256": "f0b3809aa1831291d63c90863d97a4ecf377dc9aca28f8d11ad3f45524472b17" + }, + { + "ordinal": 3, + "sha256": "5ef4baffb490d42a95836cec0c6f2fa1c07a2f5580f9d67e193dc57dd2f7d15c" + }, + { + "ordinal": 4, + "sha256": "1204c6fa91f42043f9fb2e45a8ef82fd306295085fb7fc21f587887df5dfdac1" + } + ] + }, + { + "id": "58", + "requirement": "OCAI 分析结果就地展示:点击“OCAI 分析”后不能直接跳聊天页,而是在当前任务或节点 Drawer 内返回分析结果。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryOCAIAnalysisPanel.vue", + "manager/backend/services/footstone/application/repository_ocai.go", + "manager/backend/services/footstone/domain/repositoryAIService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/application/repository_ocai_test.go", + "manager/backend/services/footstone/domain/repositoryAIService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 2, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "5b14facd5bc39dcb5d1779c2354c6ebfc4fee1d39f8247612407ff06da329174" + }, + { + "ordinal": 2, + "sha256": "93b034b4e551dccac3c567922319e46ee8e00b6a5b458f1367accb3c114c4c68" + } + ] + }, + { + "id": "59", + "requirement": "OCAI 分析提供加载、失败和重新分析状态。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryOCAIAnalysisPanel.vue", + "manager/backend/services/footstone/application/repository_ocai.go", + "manager/backend/services/footstone/domain/repositoryAIService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/application/repository_ocai_test.go", + "manager/backend/services/footstone/domain/repositoryAIService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 2, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "5b14facd5bc39dcb5d1779c2354c6ebfc4fee1d39f8247612407ff06da329174" + }, + { + "ordinal": 2, + "sha256": "93b034b4e551dccac3c567922319e46ee8e00b6a5b458f1367accb3c114c4c68" + } + ] + }, + { + "id": "60", + "requirement": "保留“在 OCAI 中继续分析”入口:只有用户需要进一步追问时再进入完整聊天页面。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryOCAIAnalysisPanel.vue", + "manager/backend/services/footstone/application/repository_ocai.go", + "manager/backend/services/footstone/domain/repositoryAIService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/application/repository_ocai_test.go", + "manager/backend/services/footstone/domain/repositoryAIService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 2, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "5b14facd5bc39dcb5d1779c2354c6ebfc4fee1d39f8247612407ff06da329174" + }, + { + "ordinal": 2, + "sha256": "93b034b4e551dccac3c567922319e46ee8e00b6a5b458f1367accb3c114c4c68" + } + ] + }, + { + "id": "61", + "requirement": "用户可见错误信息统一中文化:任务失败原因、处理建议、隔离区拒绝原因、节点预检说明等不得再大段中英文混杂。", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/domain/repositoryErrorCatalog/catalog.go", + "manager/backend/services/footstone/presentation/repository_error_catalog.go", + "manager/frontend/src/pages/repository/utils.ts" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryErrorCatalog/catalog_test.go", + "manager/backend/services/footstone/presentation/repository_error_catalog_test.go", + "manager/frontend/src/pages/repository/utils.test.ts" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 2, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "8685d020c0525fdca2bd4bfd7058eb7064a673bc092f2330b174786f8886c856" + }, + { + "ordinal": 2, + "sha256": "184f9160f3e0cf7648d651448d45c17cde666ad5938ed1d15781f79ee402571e" + } + ] + }, + { + "id": "62", + "requirement": "错误码继续保留英文原码:例如 NODE_INSUFFICIENT_SPACE 不翻译、不删除,便于日志检索。", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/domain/repositoryErrorCatalog/catalog.go", + "manager/backend/services/footstone/presentation/repository_error_catalog.go", + "manager/frontend/src/pages/repository/utils.ts" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryErrorCatalog/catalog_test.go", + "manager/backend/services/footstone/presentation/repository_error_catalog_test.go", + "manager/frontend/src/pages/repository/utils.test.ts" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 2, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "8685d020c0525fdca2bd4bfd7058eb7064a673bc092f2330b174786f8886c856" + }, + { + "ordinal": 2, + "sha256": "184f9160f3e0cf7648d651448d45c17cde666ad5938ed1d15781f79ee402571e" + } + ] + }, + { + "id": "63", + "requirement": "错误体系至少同时包含 Code、中文说明和中文处理建议,不要让每个 Vue 页面自己硬编码英文字符串翻译。", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/domain/repositoryErrorCatalog/catalog.go", + "manager/backend/services/footstone/presentation/repository_error_catalog.go", + "manager/frontend/src/pages/repository/utils.ts" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryErrorCatalog/catalog_test.go", + "manager/backend/services/footstone/presentation/repository_error_catalog_test.go", + "manager/frontend/src/pages/repository/utils.test.ts" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 2, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "8685d020c0525fdca2bd4bfd7058eb7064a673bc092f2330b174786f8886c856" + }, + { + "ordinal": 2, + "sha256": "184f9160f3e0cf7648d651448d45c17cde666ad5938ed1d15781f79ee402571e" + } + ] + }, + { + "id": "64", + "requirement": "软件包支持通过 TMS 批量安装到节点:可在软件包列表选择多个包,再选择多个目标节点执行安装。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.vue", + "manager/backend/services/footstone/application/repository_package_install.go", + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 8, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "cccf69cd5f83814d6a7ac6432069acd64bacd669940d9da9a4913ee7939d3d7e" + }, + { + "ordinal": 2, + "sha256": "419363cf7a380ab38c906e96f3b2db1ea8de719b50cdbef65b50e1b25bcc8055" + }, + { + "ordinal": 3, + "sha256": "857209f60de023e1c93ee0b31ba9c56c7639976fe7e2cdb67ec9df8ee7a6b48f" + }, + { + "ordinal": 4, + "sha256": "6dbd7f316490f6c529f9188d91cdee180fc3948353b21692964f2adabf6c7644" + }, + { + "ordinal": 5, + "sha256": "528e3240e44f369a87a9bcfc62dad7817cc31b1a6b1acaf77330030dba361d77" + }, + { + "ordinal": 6, + "sha256": "b784f8f08576365b90141ad36cdaf12d80e068483c8cb11333df3db142ea327b" + }, + { + "ordinal": 7, + "sha256": "4309f85860718627bdfd7e2d483089e876f6e548a3373905c56bb10f655ffad4" + }, + { + "ordinal": 8, + "sha256": "15cdeb215df3a5cd2c5c5a0339f655ede790daefb974932750a3f972ca346173" + } + ] + }, + { + "id": "65", + "requirement": "批量安装必须先做 Transaction Preview:绝对不能点击后直接执行 dnf install -y。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.vue", + "manager/backend/services/footstone/application/repository_package_install.go", + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 8, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "cccf69cd5f83814d6a7ac6432069acd64bacd669940d9da9a4913ee7939d3d7e" + }, + { + "ordinal": 2, + "sha256": "419363cf7a380ab38c906e96f3b2db1ea8de719b50cdbef65b50e1b25bcc8055" + }, + { + "ordinal": 3, + "sha256": "857209f60de023e1c93ee0b31ba9c56c7639976fe7e2cdb67ec9df8ee7a6b48f" + }, + { + "ordinal": 4, + "sha256": "6dbd7f316490f6c529f9188d91cdee180fc3948353b21692964f2adabf6c7644" + }, + { + "ordinal": 5, + "sha256": "528e3240e44f369a87a9bcfc62dad7817cc31b1a6b1acaf77330030dba361d77" + }, + { + "ordinal": 6, + "sha256": "b784f8f08576365b90141ad36cdaf12d80e068483c8cb11333df3db142ea327b" + }, + { + "ordinal": 7, + "sha256": "4309f85860718627bdfd7e2d483089e876f6e548a3373905c56bb10f655ffad4" + }, + { + "ordinal": 8, + "sha256": "15cdeb215df3a5cd2c5c5a0339f655ede790daefb974932750a3f972ca346173" + } + ] + }, + { + "id": "66", + "requirement": "预演结果按机器分别展示:因为不同节点当前安装状态不同。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.vue", + "manager/backend/services/footstone/application/repository_package_install.go", + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 8, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "cccf69cd5f83814d6a7ac6432069acd64bacd669940d9da9a4913ee7939d3d7e" + }, + { + "ordinal": 2, + "sha256": "419363cf7a380ab38c906e96f3b2db1ea8de719b50cdbef65b50e1b25bcc8055" + }, + { + "ordinal": 3, + "sha256": "857209f60de023e1c93ee0b31ba9c56c7639976fe7e2cdb67ec9df8ee7a6b48f" + }, + { + "ordinal": 4, + "sha256": "6dbd7f316490f6c529f9188d91cdee180fc3948353b21692964f2adabf6c7644" + }, + { + "ordinal": 5, + "sha256": "528e3240e44f369a87a9bcfc62dad7817cc31b1a6b1acaf77330030dba361d77" + }, + { + "ordinal": 6, + "sha256": "b784f8f08576365b90141ad36cdaf12d80e068483c8cb11333df3db142ea327b" + }, + { + "ordinal": 7, + "sha256": "4309f85860718627bdfd7e2d483089e876f6e548a3373905c56bb10f655ffad4" + }, + { + "ordinal": 8, + "sha256": "15cdeb215df3a5cd2c5c5a0339f655ede790daefb974932750a3f972ca346173" + } + ] + }, + { + "id": "67", + "requirement": "安装预演至少展示用户选择包、额外依赖包、将升级的软件包、可能替换的软件包、预计下载量或空间变化。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.vue", + "manager/backend/services/footstone/application/repository_package_install.go", + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 8, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "cccf69cd5f83814d6a7ac6432069acd64bacd669940d9da9a4913ee7939d3d7e" + }, + { + "ordinal": 2, + "sha256": "419363cf7a380ab38c906e96f3b2db1ea8de719b50cdbef65b50e1b25bcc8055" + }, + { + "ordinal": 3, + "sha256": "857209f60de023e1c93ee0b31ba9c56c7639976fe7e2cdb67ec9df8ee7a6b48f" + }, + { + "ordinal": 4, + "sha256": "6dbd7f316490f6c529f9188d91cdee180fc3948353b21692964f2adabf6c7644" + }, + { + "ordinal": 5, + "sha256": "528e3240e44f369a87a9bcfc62dad7817cc31b1a6b1acaf77330030dba361d77" + }, + { + "ordinal": 6, + "sha256": "b784f8f08576365b90141ad36cdaf12d80e068483c8cb11333df3db142ea327b" + }, + { + "ordinal": 7, + "sha256": "4309f85860718627bdfd7e2d483089e876f6e548a3373905c56bb10f655ffad4" + }, + { + "ordinal": 8, + "sha256": "15cdeb215df3a5cd2c5c5a0339f655ede790daefb974932750a3f972ca346173" + } + ] + }, + { + "id": "68", + "requirement": "用户确认预演结果后才能正式安装。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.vue", + "manager/backend/services/footstone/application/repository_package_install.go", + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 8, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "cccf69cd5f83814d6a7ac6432069acd64bacd669940d9da9a4913ee7939d3d7e" + }, + { + "ordinal": 2, + "sha256": "419363cf7a380ab38c906e96f3b2db1ea8de719b50cdbef65b50e1b25bcc8055" + }, + { + "ordinal": 3, + "sha256": "857209f60de023e1c93ee0b31ba9c56c7639976fe7e2cdb67ec9df8ee7a6b48f" + }, + { + "ordinal": 4, + "sha256": "6dbd7f316490f6c529f9188d91cdee180fc3948353b21692964f2adabf6c7644" + }, + { + "ordinal": 5, + "sha256": "528e3240e44f369a87a9bcfc62dad7817cc31b1a6b1acaf77330030dba361d77" + }, + { + "ordinal": 6, + "sha256": "b784f8f08576365b90141ad36cdaf12d80e068483c8cb11333df3db142ea327b" + }, + { + "ordinal": 7, + "sha256": "4309f85860718627bdfd7e2d483089e876f6e548a3373905c56bb10f655ffad4" + }, + { + "ordinal": 8, + "sha256": "15cdeb215df3a5cd2c5c5a0339f655ede790daefb974932750a3f972ca346173" + } + ] + }, + { + "id": "69", + "requirement": "批量安装执行结果按节点分别展示:每台节点必须明确成功或失败。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.vue", + "manager/backend/services/footstone/application/repository_package_install.go", + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 8, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "cccf69cd5f83814d6a7ac6432069acd64bacd669940d9da9a4913ee7939d3d7e" + }, + { + "ordinal": 2, + "sha256": "419363cf7a380ab38c906e96f3b2db1ea8de719b50cdbef65b50e1b25bcc8055" + }, + { + "ordinal": 3, + "sha256": "857209f60de023e1c93ee0b31ba9c56c7639976fe7e2cdb67ec9df8ee7a6b48f" + }, + { + "ordinal": 4, + "sha256": "6dbd7f316490f6c529f9188d91cdee180fc3948353b21692964f2adabf6c7644" + }, + { + "ordinal": 5, + "sha256": "528e3240e44f369a87a9bcfc62dad7817cc31b1a6b1acaf77330030dba361d77" + }, + { + "ordinal": 6, + "sha256": "b784f8f08576365b90141ad36cdaf12d80e068483c8cb11333df3db142ea327b" + }, + { + "ordinal": 7, + "sha256": "4309f85860718627bdfd7e2d483089e876f6e548a3373905c56bb10f655ffad4" + }, + { + "ordinal": 8, + "sha256": "15cdeb215df3a5cd2c5c5a0339f655ede790daefb974932750a3f972ca346173" + } + ] + }, + { + "id": "70", + "requirement": "失败节点优先展示:便于快速排障。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.vue", + "manager/backend/services/footstone/application/repository_package_install.go", + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 8, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "cccf69cd5f83814d6a7ac6432069acd64bacd669940d9da9a4913ee7939d3d7e" + }, + { + "ordinal": 2, + "sha256": "419363cf7a380ab38c906e96f3b2db1ea8de719b50cdbef65b50e1b25bcc8055" + }, + { + "ordinal": 3, + "sha256": "857209f60de023e1c93ee0b31ba9c56c7639976fe7e2cdb67ec9df8ee7a6b48f" + }, + { + "ordinal": 4, + "sha256": "6dbd7f316490f6c529f9188d91cdee180fc3948353b21692964f2adabf6c7644" + }, + { + "ordinal": 5, + "sha256": "528e3240e44f369a87a9bcfc62dad7817cc31b1a6b1acaf77330030dba361d77" + }, + { + "ordinal": 6, + "sha256": "b784f8f08576365b90141ad36cdaf12d80e068483c8cb11333df3db142ea327b" + }, + { + "ordinal": 7, + "sha256": "4309f85860718627bdfd7e2d483089e876f6e548a3373905c56bb10f655ffad4" + }, + { + "ordinal": 8, + "sha256": "15cdeb215df3a5cd2c5c5a0339f655ede790daefb974932750a3f972ca346173" + } + ] + }, + { + "id": "71", + "requirement": "批量安装失败必须展示错误码、中文原因和必要日志摘要。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.vue", + "manager/backend/services/footstone/application/repository_package_install.go", + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 8, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "cccf69cd5f83814d6a7ac6432069acd64bacd669940d9da9a4913ee7939d3d7e" + }, + { + "ordinal": 2, + "sha256": "419363cf7a380ab38c906e96f3b2db1ea8de719b50cdbef65b50e1b25bcc8055" + }, + { + "ordinal": 3, + "sha256": "857209f60de023e1c93ee0b31ba9c56c7639976fe7e2cdb67ec9df8ee7a6b48f" + }, + { + "ordinal": 4, + "sha256": "6dbd7f316490f6c529f9188d91cdee180fc3948353b21692964f2adabf6c7644" + }, + { + "ordinal": 5, + "sha256": "528e3240e44f369a87a9bcfc62dad7817cc31b1a6b1acaf77330030dba361d77" + }, + { + "ordinal": 6, + "sha256": "b784f8f08576365b90141ad36cdaf12d80e068483c8cb11333df3db142ea327b" + }, + { + "ordinal": 7, + "sha256": "4309f85860718627bdfd7e2d483089e876f6e548a3373905c56bb10f655ffad4" + }, + { + "ordinal": 8, + "sha256": "15cdeb215df3a5cd2c5c5a0339f655ede790daefb974932750a3f972ca346173" + } + ] + }, + { + "id": "72", + "requirement": "软件包批量安装产生的执行记录必须进入统一任务总览。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.vue", + "manager/backend/services/footstone/application/repository_package_install.go", + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 8, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "cccf69cd5f83814d6a7ac6432069acd64bacd669940d9da9a4913ee7939d3d7e" + }, + { + "ordinal": 2, + "sha256": "419363cf7a380ab38c906e96f3b2db1ea8de719b50cdbef65b50e1b25bcc8055" + }, + { + "ordinal": 3, + "sha256": "857209f60de023e1c93ee0b31ba9c56c7639976fe7e2cdb67ec9df8ee7a6b48f" + }, + { + "ordinal": 4, + "sha256": "6dbd7f316490f6c529f9188d91cdee180fc3948353b21692964f2adabf6c7644" + }, + { + "ordinal": 5, + "sha256": "528e3240e44f369a87a9bcfc62dad7817cc31b1a6b1acaf77330030dba361d77" + }, + { + "ordinal": 6, + "sha256": "b784f8f08576365b90141ad36cdaf12d80e068483c8cb11333df3db142ea327b" + }, + { + "ordinal": 7, + "sha256": "4309f85860718627bdfd7e2d483089e876f6e548a3373905c56bb10f655ffad4" + }, + { + "ordinal": 8, + "sha256": "15cdeb215df3a5cd2c5c5a0339f655ede790daefb974932750a3f972ca346173" + } + ] + }, + { + "id": "73", + "requirement": "仓库同步支持定时执行:至少支持每天和每周两种常见配置。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositorySyncSchedulePanel.vue", + "manager/backend/services/footstone/domain/repositoryScheduleService/service.go", + "manager/backend/services/footstone/domain/repository/repositoryschedulerepo/repository.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryScheduleService/service_test.go", + "manager/backend/services/footstone/domain/repositoryScheduleService/service_integration_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 2, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "857209f60de023e1c93ee0b31ba9c56c7639976fe7e2cdb67ec9df8ee7a6b48f" + } + ] + }, + { + "id": "74", + "requirement": "前端不强迫用户直接填写 Cron:以人可读方式配置时间。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositorySyncSchedulePanel.vue", + "manager/backend/services/footstone/domain/repositoryScheduleService/service.go", + "manager/backend/services/footstone/domain/repository/repositoryschedulerepo/repository.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryScheduleService/service_test.go", + "manager/backend/services/footstone/domain/repositoryScheduleService/service_integration_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 2, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "857209f60de023e1c93ee0b31ba9c56c7639976fe7e2cdb67ec9df8ee7a6b48f" + } + ] + }, + { + "id": "75", + "requirement": "定时同步配置完成后显示“下次执行时间”:用户不需要自行计算 Cron。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositorySyncSchedulePanel.vue", + "manager/backend/services/footstone/domain/repositoryScheduleService/service.go", + "manager/backend/services/footstone/domain/repository/repositoryschedulerepo/repository.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryScheduleService/service_test.go", + "manager/backend/services/footstone/domain/repositoryScheduleService/service_integration_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 2, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "857209f60de023e1c93ee0b31ba9c56c7639976fe7e2cdb67ec9df8ee7a6b48f" + } + ] + }, + { + "id": "76", + "requirement": "同一仓库上一次同步仍在执行时,不得再次启动新的同步任务。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositorySyncSchedulePanel.vue", + "manager/backend/services/footstone/domain/repositoryScheduleService/service.go", + "manager/backend/services/footstone/domain/repository/repositoryschedulerepo/repository.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryScheduleService/service_test.go", + "manager/backend/services/footstone/domain/repositoryScheduleService/service_integration_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 2, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "857209f60de023e1c93ee0b31ba9c56c7639976fe7e2cdb67ec9df8ee7a6b48f" + } + ] + }, + { + "id": "77", + "requirement": "定时同步具备连续失败保护:连续失败达到阈值后自动暂停或禁用该 Schedule,并产生明显告警。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositorySyncSchedulePanel.vue", + "manager/backend/services/footstone/domain/repositoryScheduleService/service.go", + "manager/backend/services/footstone/domain/repository/repositoryschedulerepo/repository.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryScheduleService/service_test.go", + "manager/backend/services/footstone/domain/repositoryScheduleService/service_integration_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 2, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "857209f60de023e1c93ee0b31ba9c56c7639976fe7e2cdb67ec9df8ee7a6b48f" + } + ] + }, + { + "id": "78", + "requirement": "不得因上游长期不可用而不断生成大量重复失败任务。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositorySyncSchedulePanel.vue", + "manager/backend/services/footstone/domain/repositoryScheduleService/service.go", + "manager/backend/services/footstone/domain/repository/repositoryschedulerepo/repository.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryScheduleService/service_test.go", + "manager/backend/services/footstone/domain/repositoryScheduleService/service_integration_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 2, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "857209f60de023e1c93ee0b31ba9c56c7639976fe7e2cdb67ec9df8ee7a6b48f" + } + ] + }, + { + "id": "79", + "requirement": "任务列表明确区分人工同步和定时同步。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositorySyncSchedulePanel.vue", + "manager/backend/services/footstone/domain/repositoryScheduleService/service.go", + "manager/backend/services/footstone/domain/repository/repositoryschedulerepo/repository.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryScheduleService/service_test.go", + "manager/backend/services/footstone/domain/repositoryScheduleService/service_integration_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 2, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "857209f60de023e1c93ee0b31ba9c56c7639976fe7e2cdb67ec9df8ee7a6b48f" + } + ] + }, + { + "id": "80", + "requirement": "现有分片上传升级为真正断点续传:不能只是拥有 chunk API,但断线后仍然从 0 开始。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/frontend/src/pages/repository/upload.ts", + "manager/backend/services/footstone/domain/repositoryContentService/service.go", + "manager/backend/services/footstone/infrastructure/repository/storage/storage.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/upload.test.ts", + "manager/backend/services/footstone/infrastructure/repository/storage/storage_test.go", + "manager/backend/services/footstone/infrastructure/repository/nodeclient/client_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "49c2ac6554a2705c98baef5686383e421db9702c4b39342849896529b425f716" + }, + { + "ordinal": 2, + "sha256": "af1d61a06bb62d290ad8a681ab0f8b25610988aa171a4b5637b65052f9a17872" + }, + { + "ordinal": 3, + "sha256": "f45816d8e28eb530123e322105df3a582dceeb18d895601e3cf0ed3f16dd7a28" + } + ] + }, + { + "id": "81", + "requirement": "服务器保存 Upload Session 状态:至少能识别文件、总大小、Chunk 大小、已上传块和当前状态。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/frontend/src/pages/repository/upload.ts", + "manager/backend/services/footstone/domain/repositoryContentService/service.go", + "manager/backend/services/footstone/infrastructure/repository/storage/storage.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/upload.test.ts", + "manager/backend/services/footstone/infrastructure/repository/storage/storage_test.go", + "manager/backend/services/footstone/infrastructure/repository/nodeclient/client_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "49c2ac6554a2705c98baef5686383e421db9702c4b39342849896529b425f716" + }, + { + "ordinal": 2, + "sha256": "af1d61a06bb62d290ad8a681ab0f8b25610988aa171a4b5637b65052f9a17872" + }, + { + "ordinal": 3, + "sha256": "f45816d8e28eb530123e322105df3a582dceeb18d895601e3cf0ed3f16dd7a28" + } + ] + }, + { + "id": "82", + "requirement": "网络中断后重新选择同一文件能够继续上传缺失分片,不能重新传已完成部分。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/frontend/src/pages/repository/upload.ts", + "manager/backend/services/footstone/domain/repositoryContentService/service.go", + "manager/backend/services/footstone/infrastructure/repository/storage/storage.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/upload.test.ts", + "manager/backend/services/footstone/infrastructure/repository/storage/storage_test.go", + "manager/backend/services/footstone/infrastructure/repository/nodeclient/client_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "49c2ac6554a2705c98baef5686383e421db9702c4b39342849896529b425f716" + }, + { + "ordinal": 2, + "sha256": "af1d61a06bb62d290ad8a681ab0f8b25610988aa171a4b5637b65052f9a17872" + }, + { + "ordinal": 3, + "sha256": "f45816d8e28eb530123e322105df3a582dceeb18d895601e3cf0ed3f16dd7a28" + } + ] + }, + { + "id": "83", + "requirement": "上传支持人工暂停。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/frontend/src/pages/repository/upload.ts", + "manager/backend/services/footstone/domain/repositoryContentService/service.go", + "manager/backend/services/footstone/infrastructure/repository/storage/storage.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/upload.test.ts", + "manager/backend/services/footstone/infrastructure/repository/storage/storage_test.go", + "manager/backend/services/footstone/infrastructure/repository/nodeclient/client_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "49c2ac6554a2705c98baef5686383e421db9702c4b39342849896529b425f716" + }, + { + "ordinal": 2, + "sha256": "af1d61a06bb62d290ad8a681ab0f8b25610988aa171a4b5637b65052f9a17872" + }, + { + "ordinal": 3, + "sha256": "f45816d8e28eb530123e322105df3a582dceeb18d895601e3cf0ed3f16dd7a28" + } + ] + }, + { + "id": "84", + "requirement": "上传支持人工继续。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/frontend/src/pages/repository/upload.ts", + "manager/backend/services/footstone/domain/repositoryContentService/service.go", + "manager/backend/services/footstone/infrastructure/repository/storage/storage.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/upload.test.ts", + "manager/backend/services/footstone/infrastructure/repository/storage/storage_test.go", + "manager/backend/services/footstone/infrastructure/repository/nodeclient/client_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "49c2ac6554a2705c98baef5686383e421db9702c4b39342849896529b425f716" + }, + { + "ordinal": 2, + "sha256": "af1d61a06bb62d290ad8a681ab0f8b25610988aa171a4b5637b65052f9a17872" + }, + { + "ordinal": 3, + "sha256": "f45816d8e28eb530123e322105df3a582dceeb18d895601e3cf0ed3f16dd7a28" + } + ] + }, + { + "id": "85", + "requirement": "上传支持取消操作。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/frontend/src/pages/repository/upload.ts", + "manager/backend/services/footstone/domain/repositoryContentService/service.go", + "manager/backend/services/footstone/infrastructure/repository/storage/storage.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/upload.test.ts", + "manager/backend/services/footstone/infrastructure/repository/storage/storage_test.go", + "manager/backend/services/footstone/infrastructure/repository/nodeclient/client_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "49c2ac6554a2705c98baef5686383e421db9702c4b39342849896529b425f716" + }, + { + "ordinal": 2, + "sha256": "af1d61a06bb62d290ad8a681ab0f8b25610988aa171a4b5637b65052f9a17872" + }, + { + "ordinal": 3, + "sha256": "f45816d8e28eb530123e322105df3a582dceeb18d895601e3cf0ed3f16dd7a28" + } + ] + }, + { + "id": "86", + "requirement": "多文件上传时有队列管理。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/frontend/src/pages/repository/upload.ts", + "manager/backend/services/footstone/domain/repositoryContentService/service.go", + "manager/backend/services/footstone/infrastructure/repository/storage/storage.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/upload.test.ts", + "manager/backend/services/footstone/infrastructure/repository/storage/storage_test.go", + "manager/backend/services/footstone/infrastructure/repository/nodeclient/client_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "49c2ac6554a2705c98baef5686383e421db9702c4b39342849896529b425f716" + }, + { + "ordinal": 2, + "sha256": "af1d61a06bb62d290ad8a681ab0f8b25610988aa171a4b5637b65052f9a17872" + }, + { + "ordinal": 3, + "sha256": "f45816d8e28eb530123e322105df3a582dceeb18d895601e3cf0ed3f16dd7a28" + } + ] + }, + { + "id": "87", + "requirement": "多文件上传展示单文件进度和总体进度。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/frontend/src/pages/repository/upload.ts", + "manager/backend/services/footstone/domain/repositoryContentService/service.go", + "manager/backend/services/footstone/infrastructure/repository/storage/storage.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/upload.test.ts", + "manager/backend/services/footstone/infrastructure/repository/storage/storage_test.go", + "manager/backend/services/footstone/infrastructure/repository/nodeclient/client_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "49c2ac6554a2705c98baef5686383e421db9702c4b39342849896529b425f716" + }, + { + "ordinal": 2, + "sha256": "af1d61a06bb62d290ad8a681ab0f8b25610988aa171a4b5637b65052f9a17872" + }, + { + "ordinal": 3, + "sha256": "f45816d8e28eb530123e322105df3a582dceeb18d895601e3cf0ed3f16dd7a28" + } + ] + }, + { + "id": "88", + "requirement": "超大文件不得强制浏览器先计算完整 SHA256 才允许开始上传;最终服务端仍须完成可靠完整性校验。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/frontend/src/pages/repository/upload.ts", + "manager/backend/services/footstone/domain/repositoryContentService/service.go", + "manager/backend/services/footstone/infrastructure/repository/storage/storage.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/upload.test.ts", + "manager/backend/services/footstone/infrastructure/repository/storage/storage_test.go", + "manager/backend/services/footstone/infrastructure/repository/nodeclient/client_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "49c2ac6554a2705c98baef5686383e421db9702c4b39342849896529b425f716" + }, + { + "ordinal": 2, + "sha256": "af1d61a06bb62d290ad8a681ab0f8b25610988aa171a4b5637b65052f9a17872" + }, + { + "ordinal": 3, + "sha256": "f45816d8e28eb530123e322105df3a582dceeb18d895601e3cf0ed3f16dd7a28" + } + ] + }, + { + "id": "89", + "requirement": "删除软件包之前必须由后端执行依赖影响分析。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryContentService/package_delete.go", + "manager/backend/services/footstone/repositoryworker/remote_delete_executor.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryContentService/package_delete_test.go", + "manager/backend/services/footstone/repositoryworker/remote_delete_executor_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 7, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "82fcc8ff121cf8f3f7d1e803ef947e93cf673e0e29d74a5b3f7a992f5bf17501" + }, + { + "ordinal": 2, + "sha256": "f45816d8e28eb530123e322105df3a582dceeb18d895601e3cf0ed3f16dd7a28" + }, + { + "ordinal": 3, + "sha256": "3352e00ab60ef2f96a9c7fbacb6863e1666da549ef88d84349f623a63920d13c" + }, + { + "ordinal": 4, + "sha256": "57880eb8d961e360c0306ee531528f730792b3704421f2c375dc06fa3b75a831" + }, + { + "ordinal": 5, + "sha256": "8bbf97c7a20fde8fc2cf4a0caee457dc4210ef72623e96c22b26037828c4f82f" + }, + { + "ordinal": 6, + "sha256": "75af347a531ca21bdbaba621a9d3bee7a691a041d67abf18d6d2055700147996" + }, + { + "ordinal": 7, + "sha256": "51e446920f5adc322020610cd3d1b7f1ef73f8e8bc50661e3b0b3092e6a9152c" + } + ] + }, + { + "id": "90", + "requirement": "依赖分析至少识别当前仓库中哪些软件包直接依赖待删包。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryContentService/package_delete.go", + "manager/backend/services/footstone/repositoryworker/remote_delete_executor.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryContentService/package_delete_test.go", + "manager/backend/services/footstone/repositoryworker/remote_delete_executor_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 7, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "82fcc8ff121cf8f3f7d1e803ef947e93cf673e0e29d74a5b3f7a992f5bf17501" + }, + { + "ordinal": 2, + "sha256": "f45816d8e28eb530123e322105df3a582dceeb18d895601e3cf0ed3f16dd7a28" + }, + { + "ordinal": 3, + "sha256": "3352e00ab60ef2f96a9c7fbacb6863e1666da549ef88d84349f623a63920d13c" + }, + { + "ordinal": 4, + "sha256": "57880eb8d961e360c0306ee531528f730792b3704421f2c375dc06fa3b75a831" + }, + { + "ordinal": 5, + "sha256": "8bbf97c7a20fde8fc2cf4a0caee457dc4210ef72623e96c22b26037828c4f82f" + }, + { + "ordinal": 6, + "sha256": "75af347a531ca21bdbaba621a9d3bee7a691a041d67abf18d6d2055700147996" + }, + { + "ordinal": 7, + "sha256": "51e446920f5adc322020610cd3d1b7f1ef73f8e8bc50661e3b0b3092e6a9152c" + } + ] + }, + { + "id": "91", + "requirement": "具备条件时应计算传递依赖影响,并在确认界面中明确展示。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryContentService/package_delete.go", + "manager/backend/services/footstone/repositoryworker/remote_delete_executor.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryContentService/package_delete_test.go", + "manager/backend/services/footstone/repositoryworker/remote_delete_executor_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 7, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "82fcc8ff121cf8f3f7d1e803ef947e93cf673e0e29d74a5b3f7a992f5bf17501" + }, + { + "ordinal": 2, + "sha256": "f45816d8e28eb530123e322105df3a582dceeb18d895601e3cf0ed3f16dd7a28" + }, + { + "ordinal": 3, + "sha256": "3352e00ab60ef2f96a9c7fbacb6863e1666da549ef88d84349f623a63920d13c" + }, + { + "ordinal": 4, + "sha256": "57880eb8d961e360c0306ee531528f730792b3704421f2c375dc06fa3b75a831" + }, + { + "ordinal": 5, + "sha256": "8bbf97c7a20fde8fc2cf4a0caee457dc4210ef72623e96c22b26037828c4f82f" + }, + { + "ordinal": 6, + "sha256": "75af347a531ca21bdbaba621a9d3bee7a691a041d67abf18d6d2055700147996" + }, + { + "ordinal": 7, + "sha256": "51e446920f5adc322020610cd3d1b7f1ef73f8e8bc50661e3b0b3092e6a9152c" + } + ] + }, + { + "id": "92", + "requirement": "有依赖的软件包删除不能再只弹“确认删除吗”。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryContentService/package_delete.go", + "manager/backend/services/footstone/repositoryworker/remote_delete_executor.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryContentService/package_delete_test.go", + "manager/backend/services/footstone/repositoryworker/remote_delete_executor_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 7, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "82fcc8ff121cf8f3f7d1e803ef947e93cf673e0e29d74a5b3f7a992f5bf17501" + }, + { + "ordinal": 2, + "sha256": "f45816d8e28eb530123e322105df3a582dceeb18d895601e3cf0ed3f16dd7a28" + }, + { + "ordinal": 3, + "sha256": "3352e00ab60ef2f96a9c7fbacb6863e1666da549ef88d84349f623a63920d13c" + }, + { + "ordinal": 4, + "sha256": "57880eb8d961e360c0306ee531528f730792b3704421f2c375dc06fa3b75a831" + }, + { + "ordinal": 5, + "sha256": "8bbf97c7a20fde8fc2cf4a0caee457dc4210ef72623e96c22b26037828c4f82f" + }, + { + "ordinal": 6, + "sha256": "75af347a531ca21bdbaba621a9d3bee7a691a041d67abf18d6d2055700147996" + }, + { + "ordinal": 7, + "sha256": "51e446920f5adc322020610cd3d1b7f1ef73f8e8bc50661e3b0b3092e6a9152c" + } + ] + }, + { + "id": "93", + "requirement": "删除确认界面必须展示受影响包、数量和风险。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryContentService/package_delete.go", + "manager/backend/services/footstone/repositoryworker/remote_delete_executor.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryContentService/package_delete_test.go", + "manager/backend/services/footstone/repositoryworker/remote_delete_executor_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 7, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "82fcc8ff121cf8f3f7d1e803ef947e93cf673e0e29d74a5b3f7a992f5bf17501" + }, + { + "ordinal": 2, + "sha256": "f45816d8e28eb530123e322105df3a582dceeb18d895601e3cf0ed3f16dd7a28" + }, + { + "ordinal": 3, + "sha256": "3352e00ab60ef2f96a9c7fbacb6863e1666da549ef88d84349f623a63920d13c" + }, + { + "ordinal": 4, + "sha256": "57880eb8d961e360c0306ee531528f730792b3704421f2c375dc06fa3b75a831" + }, + { + "ordinal": 5, + "sha256": "8bbf97c7a20fde8fc2cf4a0caee457dc4210ef72623e96c22b26037828c4f82f" + }, + { + "ordinal": 6, + "sha256": "75af347a531ca21bdbaba621a9d3bee7a691a041d67abf18d6d2055700147996" + }, + { + "ordinal": 7, + "sha256": "51e446920f5adc322020610cd3d1b7f1ef73f8e8bc50661e3b0b3092e6a9152c" + } + ] + }, + { + "id": "94", + "requirement": "默认安全行为为取消删除,而不是自动级联。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryContentService/package_delete.go", + "manager/backend/services/footstone/repositoryworker/remote_delete_executor.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryContentService/package_delete_test.go", + "manager/backend/services/footstone/repositoryworker/remote_delete_executor_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 7, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "82fcc8ff121cf8f3f7d1e803ef947e93cf673e0e29d74a5b3f7a992f5bf17501" + }, + { + "ordinal": 2, + "sha256": "f45816d8e28eb530123e322105df3a582dceeb18d895601e3cf0ed3f16dd7a28" + }, + { + "ordinal": 3, + "sha256": "3352e00ab60ef2f96a9c7fbacb6863e1666da549ef88d84349f623a63920d13c" + }, + { + "ordinal": 4, + "sha256": "57880eb8d961e360c0306ee531528f730792b3704421f2c375dc06fa3b75a831" + }, + { + "ordinal": 5, + "sha256": "8bbf97c7a20fde8fc2cf4a0caee457dc4210ef72623e96c22b26037828c4f82f" + }, + { + "ordinal": 6, + "sha256": "75af347a531ca21bdbaba621a9d3bee7a691a041d67abf18d6d2055700147996" + }, + { + "ordinal": 7, + "sha256": "51e446920f5adc322020610cd3d1b7f1ef73f8e8bc50661e3b0b3092e6a9152c" + } + ] + }, + { + "id": "95", + "requirement": "如果允许级联删除,必须明确列出将额外删除哪些包,并再次确认。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryContentService/package_delete.go", + "manager/backend/services/footstone/repositoryworker/remote_delete_executor.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryContentService/package_delete_test.go", + "manager/backend/services/footstone/repositoryworker/remote_delete_executor_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 7, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "82fcc8ff121cf8f3f7d1e803ef947e93cf673e0e29d74a5b3f7a992f5bf17501" + }, + { + "ordinal": 2, + "sha256": "f45816d8e28eb530123e322105df3a582dceeb18d895601e3cf0ed3f16dd7a28" + }, + { + "ordinal": 3, + "sha256": "3352e00ab60ef2f96a9c7fbacb6863e1666da549ef88d84349f623a63920d13c" + }, + { + "ordinal": 4, + "sha256": "57880eb8d961e360c0306ee531528f730792b3704421f2c375dc06fa3b75a831" + }, + { + "ordinal": 5, + "sha256": "8bbf97c7a20fde8fc2cf4a0caee457dc4210ef72623e96c22b26037828c4f82f" + }, + { + "ordinal": 6, + "sha256": "75af347a531ca21bdbaba621a9d3bee7a691a041d67abf18d6d2055700147996" + }, + { + "ordinal": 7, + "sha256": "51e446920f5adc322020610cd3d1b7f1ef73f8e8bc50661e3b0b3092e6a9152c" + } + ] + }, + { + "id": "96", + "requirement": "不得静默删除依赖包。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryContentService/package_delete.go", + "manager/backend/services/footstone/repositoryworker/remote_delete_executor.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryContentService/package_delete_test.go", + "manager/backend/services/footstone/repositoryworker/remote_delete_executor_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 7, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "82fcc8ff121cf8f3f7d1e803ef947e93cf673e0e29d74a5b3f7a992f5bf17501" + }, + { + "ordinal": 2, + "sha256": "f45816d8e28eb530123e322105df3a582dceeb18d895601e3cf0ed3f16dd7a28" + }, + { + "ordinal": 3, + "sha256": "3352e00ab60ef2f96a9c7fbacb6863e1666da549ef88d84349f623a63920d13c" + }, + { + "ordinal": 4, + "sha256": "57880eb8d961e360c0306ee531528f730792b3704421f2c375dc06fa3b75a831" + }, + { + "ordinal": 5, + "sha256": "8bbf97c7a20fde8fc2cf4a0caee457dc4210ef72623e96c22b26037828c4f82f" + }, + { + "ordinal": 6, + "sha256": "75af347a531ca21bdbaba621a9d3bee7a691a041d67abf18d6d2055700147996" + }, + { + "ordinal": 7, + "sha256": "51e446920f5adc322020610cd3d1b7f1ef73f8e8bc50661e3b0b3092e6a9152c" + } + ] + }, + { + "id": "97", + "requirement": "前端现有“批量删除生成多个删除任务”的方式必须改掉。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryContentService/package_delete.go", + "manager/backend/services/footstone/repositoryworker/remote_delete_executor.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryContentService/package_delete_test.go", + "manager/backend/services/footstone/repositoryworker/remote_delete_executor_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 7, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "82fcc8ff121cf8f3f7d1e803ef947e93cf673e0e29d74a5b3f7a992f5bf17501" + }, + { + "ordinal": 2, + "sha256": "f45816d8e28eb530123e322105df3a582dceeb18d895601e3cf0ed3f16dd7a28" + }, + { + "ordinal": 3, + "sha256": "3352e00ab60ef2f96a9c7fbacb6863e1666da549ef88d84349f623a63920d13c" + }, + { + "ordinal": 4, + "sha256": "57880eb8d961e360c0306ee531528f730792b3704421f2c375dc06fa3b75a831" + }, + { + "ordinal": 5, + "sha256": "8bbf97c7a20fde8fc2cf4a0caee457dc4210ef72623e96c22b26037828c4f82f" + }, + { + "ordinal": 6, + "sha256": "75af347a531ca21bdbaba621a9d3bee7a691a041d67abf18d6d2055700147996" + }, + { + "ordinal": 7, + "sha256": "51e446920f5adc322020610cd3d1b7f1ef73f8e8bc50661e3b0b3092e6a9152c" + } + ] + }, + { + "id": "98", + "requirement": "批量删除必须实现为真实后端 Batch Delete Task。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryContentService/package_delete.go", + "manager/backend/services/footstone/repositoryworker/remote_delete_executor.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryContentService/package_delete_test.go", + "manager/backend/services/footstone/repositoryworker/remote_delete_executor_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 7, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "82fcc8ff121cf8f3f7d1e803ef947e93cf673e0e29d74a5b3f7a992f5bf17501" + }, + { + "ordinal": 2, + "sha256": "f45816d8e28eb530123e322105df3a582dceeb18d895601e3cf0ed3f16dd7a28" + }, + { + "ordinal": 3, + "sha256": "3352e00ab60ef2f96a9c7fbacb6863e1666da549ef88d84349f623a63920d13c" + }, + { + "ordinal": 4, + "sha256": "57880eb8d961e360c0306ee531528f730792b3704421f2c375dc06fa3b75a831" + }, + { + "ordinal": 5, + "sha256": "8bbf97c7a20fde8fc2cf4a0caee457dc4210ef72623e96c22b26037828c4f82f" + }, + { + "ordinal": 6, + "sha256": "75af347a531ca21bdbaba621a9d3bee7a691a041d67abf18d6d2055700147996" + }, + { + "ordinal": 7, + "sha256": "51e446920f5adc322020610cd3d1b7f1ef73f8e8bc50661e3b0b3092e6a9152c" + } + ] + }, + { + "id": "99", + "requirement": "一次批量删除完成后统一重建仓库元数据,不能删除一个包就执行一次 repodata rebuild。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryContentService/package_delete.go", + "manager/backend/services/footstone/repositoryworker/remote_delete_executor.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryContentService/package_delete_test.go", + "manager/backend/services/footstone/repositoryworker/remote_delete_executor_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 7, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "82fcc8ff121cf8f3f7d1e803ef947e93cf673e0e29d74a5b3f7a992f5bf17501" + }, + { + "ordinal": 2, + "sha256": "f45816d8e28eb530123e322105df3a582dceeb18d895601e3cf0ed3f16dd7a28" + }, + { + "ordinal": 3, + "sha256": "3352e00ab60ef2f96a9c7fbacb6863e1666da549ef88d84349f623a63920d13c" + }, + { + "ordinal": 4, + "sha256": "57880eb8d961e360c0306ee531528f730792b3704421f2c375dc06fa3b75a831" + }, + { + "ordinal": 5, + "sha256": "8bbf97c7a20fde8fc2cf4a0caee457dc4210ef72623e96c22b26037828c4f82f" + }, + { + "ordinal": 6, + "sha256": "75af347a531ca21bdbaba621a9d3bee7a691a041d67abf18d6d2055700147996" + }, + { + "ordinal": 7, + "sha256": "51e446920f5adc322020610cd3d1b7f1ef73f8e8bc50661e3b0b3092e6a9152c" + } + ] + }, + { + "id": "100", + "requirement": "任务总览中一次批量删除表现为一个总任务,任务详情内展示每个包的处理结果。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryContentService/package_delete.go", + "manager/backend/services/footstone/repositoryworker/remote_delete_executor.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryContentService/package_delete_test.go", + "manager/backend/services/footstone/repositoryworker/remote_delete_executor_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 7, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "82fcc8ff121cf8f3f7d1e803ef947e93cf673e0e29d74a5b3f7a992f5bf17501" + }, + { + "ordinal": 2, + "sha256": "f45816d8e28eb530123e322105df3a582dceeb18d895601e3cf0ed3f16dd7a28" + }, + { + "ordinal": 3, + "sha256": "3352e00ab60ef2f96a9c7fbacb6863e1666da549ef88d84349f623a63920d13c" + }, + { + "ordinal": 4, + "sha256": "57880eb8d961e360c0306ee531528f730792b3704421f2c375dc06fa3b75a831" + }, + { + "ordinal": 5, + "sha256": "8bbf97c7a20fde8fc2cf4a0caee457dc4210ef72623e96c22b26037828c4f82f" + }, + { + "ordinal": 6, + "sha256": "75af347a531ca21bdbaba621a9d3bee7a691a041d67abf18d6d2055700147996" + }, + { + "ordinal": 7, + "sha256": "51e446920f5adc322020610cd3d1b7f1ef73f8e8bc50661e3b0b3092e6a9152c" + } + ] + }, + { + "id": "101", + "requirement": "仓库同步支持带宽上限配置。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryForm.vue", + "manager/backend/services/footstone/repositoryworker/sync.go", + "manager/backend/services/footstone/domain/repositoryService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/sync_command_test.go", + "manager/backend/services/footstone/domain/repositoryService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "857209f60de023e1c93ee0b31ba9c56c7639976fe7e2cdb67ec9df8ee7a6b48f" + }, + { + "ordinal": 2, + "sha256": "252857571a4ca1334a89df0467db1ca559c284c65b057cc057be88beb3ba6a5a" + }, + { + "ordinal": 3, + "sha256": "f603bd497c902611d7a55947a31b99b65e7a805f270a3582f612a75893be8d20" + } + ] + }, + { + "id": "102", + "requirement": "仓库同步支持并发数配置。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryForm.vue", + "manager/backend/services/footstone/repositoryworker/sync.go", + "manager/backend/services/footstone/domain/repositoryService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/sync_command_test.go", + "manager/backend/services/footstone/domain/repositoryService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "857209f60de023e1c93ee0b31ba9c56c7639976fe7e2cdb67ec9df8ee7a6b48f" + }, + { + "ordinal": 2, + "sha256": "252857571a4ca1334a89df0467db1ca559c284c65b057cc057be88beb3ba6a5a" + }, + { + "ordinal": 3, + "sha256": "f603bd497c902611d7a55947a31b99b65e7a805f270a3582f612a75893be8d20" + } + ] + }, + { + "id": "103", + "requirement": "带宽限制和并发限制必须在后端/Worker 真正生效,不能只在前端保存一个字段。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryForm.vue", + "manager/backend/services/footstone/repositoryworker/sync.go", + "manager/backend/services/footstone/domain/repositoryService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/sync_command_test.go", + "manager/backend/services/footstone/domain/repositoryService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "857209f60de023e1c93ee0b31ba9c56c7639976fe7e2cdb67ec9df8ee7a6b48f" + }, + { + "ordinal": 2, + "sha256": "252857571a4ca1334a89df0467db1ca559c284c65b057cc057be88beb3ba6a5a" + }, + { + "ordinal": 3, + "sha256": "f603bd497c902611d7a55947a31b99b65e7a805f270a3582f612a75893be8d20" + } + ] + }, + { + "id": "104", + "requirement": "没有设置限制时必须兼容当前同步行为。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryForm.vue", + "manager/backend/services/footstone/repositoryworker/sync.go", + "manager/backend/services/footstone/domain/repositoryService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/sync_command_test.go", + "manager/backend/services/footstone/domain/repositoryService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "857209f60de023e1c93ee0b31ba9c56c7639976fe7e2cdb67ec9df8ee7a6b48f" + }, + { + "ordinal": 2, + "sha256": "252857571a4ca1334a89df0467db1ca559c284c65b057cc057be88beb3ba6a5a" + }, + { + "ordinal": 3, + "sha256": "f603bd497c902611d7a55947a31b99b65e7a805f270a3582f612a75893be8d20" + } + ] + }, + { + "id": "105", + "requirement": "同步任务详情中能够看到本次使用的限速和并发配置。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryForm.vue", + "manager/backend/services/footstone/repositoryworker/sync.go", + "manager/backend/services/footstone/domain/repositoryService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/sync_command_test.go", + "manager/backend/services/footstone/domain/repositoryService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "857209f60de023e1c93ee0b31ba9c56c7639976fe7e2cdb67ec9df8ee7a6b48f" + }, + { + "ordinal": 2, + "sha256": "252857571a4ca1334a89df0467db1ca559c284c65b057cc057be88beb3ba6a5a" + }, + { + "ordinal": 3, + "sha256": "f603bd497c902611d7a55947a31b99b65e7a805f270a3582f612a75893be8d20" + } + ] + }, + { + "id": "106", + "requirement": "同步模式明确提供“镜像模式”和“追加模式”。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryForm.vue", + "manager/backend/services/footstone/repositoryworker/sync.go", + "manager/backend/services/footstone/domain/repositoryService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/sync_command_test.go", + "manager/backend/services/footstone/domain/repositoryService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "857209f60de023e1c93ee0b31ba9c56c7639976fe7e2cdb67ec9df8ee7a6b48f" + }, + { + "ordinal": 2, + "sha256": "252857571a4ca1334a89df0467db1ca559c284c65b057cc057be88beb3ba6a5a" + }, + { + "ordinal": 3, + "sha256": "f603bd497c902611d7a55947a31b99b65e7a805f270a3582f612a75893be8d20" + } + ] + }, + { + "id": "107", + "requirement": "镜像模式语义明确:最终本地仓库与上游保持一致,上游安全删除的软件包本地也进行对应处理。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryForm.vue", + "manager/backend/services/footstone/repositoryworker/sync.go", + "manager/backend/services/footstone/domain/repositoryService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/sync_command_test.go", + "manager/backend/services/footstone/domain/repositoryService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "857209f60de023e1c93ee0b31ba9c56c7639976fe7e2cdb67ec9df8ee7a6b48f" + }, + { + "ordinal": 2, + "sha256": "252857571a4ca1334a89df0467db1ca559c284c65b057cc057be88beb3ba6a5a" + }, + { + "ordinal": 3, + "sha256": "f603bd497c902611d7a55947a31b99b65e7a805f270a3582f612a75893be8d20" + } + ] + }, + { + "id": "108", + "requirement": "追加模式语义明确:只新增或更新,上游删除的软件包本地继续保留。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryForm.vue", + "manager/backend/services/footstone/repositoryworker/sync.go", + "manager/backend/services/footstone/domain/repositoryService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/sync_command_test.go", + "manager/backend/services/footstone/domain/repositoryService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "857209f60de023e1c93ee0b31ba9c56c7639976fe7e2cdb67ec9df8ee7a6b48f" + }, + { + "ordinal": 2, + "sha256": "252857571a4ca1334a89df0467db1ca559c284c65b057cc057be88beb3ba6a5a" + }, + { + "ordinal": 3, + "sha256": "f603bd497c902611d7a55947a31b99b65e7a805f270a3582f612a75893be8d20" + } + ] + }, + { + "id": "109", + "requirement": "同步模式在 UI 上写清楚语义,不能让用户自行猜测。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryForm.vue", + "manager/backend/services/footstone/repositoryworker/sync.go", + "manager/backend/services/footstone/domain/repositoryService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/sync_command_test.go", + "manager/backend/services/footstone/domain/repositoryService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "857209f60de023e1c93ee0b31ba9c56c7639976fe7e2cdb67ec9df8ee7a6b48f" + }, + { + "ordinal": 2, + "sha256": "252857571a4ca1334a89df0467db1ca559c284c65b057cc057be88beb3ba6a5a" + }, + { + "ordinal": 3, + "sha256": "f603bd497c902611d7a55947a31b99b65e7a805f270a3582f612a75893be8d20" + } + ] + }, + { + "id": "110", + "requirement": "老仓库升级后必须有安全默认同步模式,不得因新增字段导致行为突然变化。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryForm.vue", + "manager/backend/services/footstone/repositoryworker/sync.go", + "manager/backend/services/footstone/domain/repositoryService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/sync_command_test.go", + "manager/backend/services/footstone/domain/repositoryService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "857209f60de023e1c93ee0b31ba9c56c7639976fe7e2cdb67ec9df8ee7a6b48f" + }, + { + "ordinal": 2, + "sha256": "252857571a4ca1334a89df0467db1ca559c284c65b057cc057be88beb3ba6a5a" + }, + { + "ordinal": 3, + "sha256": "f603bd497c902611d7a55947a31b99b65e7a805f270a3582f612a75893be8d20" + } + ] + }, + { + "id": "111", + "requirement": "GPG 策略扩展为三档:关闭、校验但不拦截、严格校验。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryGPGKeys.vue", + "manager/backend/services/footstone/repositoryworker/gpg.go", + "manager/backend/services/footstone/repositoryworker/metadata_signing.go", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config.go" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/gpg_integration_test.go", + "manager/backend/services/footstone/repositoryworker/metadata_signing_test.go", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 2, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "ec23bd0fcb22934c78aa886886a7acd7f20aab55a2cbfa53f62bddbe587a4b75" + }, + { + "ordinal": 2, + "sha256": "8fc987c473cd0e4f33b3f1a373d23f660c78a626ee1b424dd303542e23ec0e13" + } + ] + }, + { + "id": "112", + "requirement": "“校验但不拦截”模式下签名问题必须记录并展示 Warning,但允许软件包进入仓库。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryGPGKeys.vue", + "manager/backend/services/footstone/repositoryworker/gpg.go", + "manager/backend/services/footstone/repositoryworker/metadata_signing.go", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config.go" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/gpg_integration_test.go", + "manager/backend/services/footstone/repositoryworker/metadata_signing_test.go", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 2, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "ec23bd0fcb22934c78aa886886a7acd7f20aab55a2cbfa53f62bddbe587a4b75" + }, + { + "ordinal": 2, + "sha256": "8fc987c473cd0e4f33b3f1a373d23f660c78a626ee1b424dd303542e23ec0e13" + } + ] + }, + { + "id": "113", + "requirement": "严格校验模式下签名异常必须拒绝或进入隔离区。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryGPGKeys.vue", + "manager/backend/services/footstone/repositoryworker/gpg.go", + "manager/backend/services/footstone/repositoryworker/metadata_signing.go", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config.go" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/gpg_integration_test.go", + "manager/backend/services/footstone/repositoryworker/metadata_signing_test.go", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 2, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "ec23bd0fcb22934c78aa886886a7acd7f20aab55a2cbfa53f62bddbe587a4b75" + }, + { + "ordinal": 2, + "sha256": "8fc987c473cd0e4f33b3f1a373d23f660c78a626ee1b424dd303542e23ec0e13" + } + ] + }, + { + "id": "114", + "requirement": "RPM 软件包签名校验和仓库元数据签名必须作为两条独立能力处理。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryGPGKeys.vue", + "manager/backend/services/footstone/repositoryworker/gpg.go", + "manager/backend/services/footstone/repositoryworker/metadata_signing.go", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config.go" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/gpg_integration_test.go", + "manager/backend/services/footstone/repositoryworker/metadata_signing_test.go", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 2, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "ec23bd0fcb22934c78aa886886a7acd7f20aab55a2cbfa53f62bddbe587a4b75" + }, + { + "ordinal": 2, + "sha256": "8fc987c473cd0e4f33b3f1a373d23f660c78a626ee1b424dd303542e23ec0e13" + } + ] + }, + { + "id": "115", + "requirement": "不能只把 .repo 中的 repo_gpgcheck 从 0 改成 1 就算完成。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryGPGKeys.vue", + "manager/backend/services/footstone/repositoryworker/gpg.go", + "manager/backend/services/footstone/repositoryworker/metadata_signing.go", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config.go" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/gpg_integration_test.go", + "manager/backend/services/footstone/repositoryworker/metadata_signing_test.go", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 2, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "ec23bd0fcb22934c78aa886886a7acd7f20aab55a2cbfa53f62bddbe587a4b75" + }, + { + "ordinal": 2, + "sha256": "8fc987c473cd0e4f33b3f1a373d23f660c78a626ee1b424dd303542e23ec0e13" + } + ] + }, + { + "id": "116", + "requirement": "Repository Metadata 必须真正完成签名链路:至少生成有效的 repomd.xml 和对应签名文件。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryGPGKeys.vue", + "manager/backend/services/footstone/repositoryworker/gpg.go", + "manager/backend/services/footstone/repositoryworker/metadata_signing.go", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config.go" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/gpg_integration_test.go", + "manager/backend/services/footstone/repositoryworker/metadata_signing_test.go", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 2, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "ec23bd0fcb22934c78aa886886a7acd7f20aab55a2cbfa53f62bddbe587a4b75" + }, + { + "ordinal": 2, + "sha256": "8fc987c473cd0e4f33b3f1a373d23f660c78a626ee1b424dd303542e23ec0e13" + } + ] + }, + { + "id": "117", + "requirement": "生成的客户端 .repo 配置中的 repo_gpgcheck 必须与服务端实际能力一致。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryGPGKeys.vue", + "manager/backend/services/footstone/repositoryworker/gpg.go", + "manager/backend/services/footstone/repositoryworker/metadata_signing.go", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config.go" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/gpg_integration_test.go", + "manager/backend/services/footstone/repositoryworker/metadata_signing_test.go", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 2, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "ec23bd0fcb22934c78aa886886a7acd7f20aab55a2cbfa53f62bddbe587a4b75" + }, + { + "ordinal": 2, + "sha256": "8fc987c473cd0e4f33b3f1a373d23f660c78a626ee1b424dd303542e23ec0e13" + } + ] + }, + { + "id": "118", + "requirement": "GPG Key 配置正确:需要校验时 .repo 中正确给出 gpgkey 等配置。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryGPGKeys.vue", + "manager/backend/services/footstone/repositoryworker/gpg.go", + "manager/backend/services/footstone/repositoryworker/metadata_signing.go", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config.go" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/gpg_integration_test.go", + "manager/backend/services/footstone/repositoryworker/metadata_signing_test.go", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 2, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "ec23bd0fcb22934c78aa886886a7acd7f20aab55a2cbfa53f62bddbe587a4b75" + }, + { + "ordinal": 2, + "sha256": "8fc987c473cd0e4f33b3f1a373d23f660c78a626ee1b424dd303542e23ec0e13" + } + ] + }, + { + "id": "119", + "requirement": "GPG 私钥不得下发到浏览器或客户端。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryGPGKeys.vue", + "manager/backend/services/footstone/repositoryworker/gpg.go", + "manager/backend/services/footstone/repositoryworker/metadata_signing.go", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config.go" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/gpg_integration_test.go", + "manager/backend/services/footstone/repositoryworker/metadata_signing_test.go", + "manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 2, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "ec23bd0fcb22934c78aa886886a7acd7f20aab55a2cbfa53f62bddbe587a4b75" + }, + { + "ordinal": 2, + "sha256": "8fc987c473cd0e4f33b3f1a373d23f660c78a626ee1b424dd303542e23ec0e13" + } + ] + }, + { + "id": "120", + "requirement": "支持仓库批量导入:不能继续只能导出不能导入。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryInventoryService/service.go", + "manager/backend/services/footstone/application/repository_report.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryInventoryService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 5, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "c35d6d462b4cc83d875ff584e2b79db3d51a654d08008ef47ac2d275d123002b" + }, + { + "ordinal": 2, + "sha256": "f45816d8e28eb530123e322105df3a582dceeb18d895601e3cf0ed3f16dd7a28" + }, + { + "ordinal": 3, + "sha256": "e577244b2ade866da4a5bf2d4447623cd205a25922f1808800f0498d9bf8df76" + }, + { + "ordinal": 4, + "sha256": "9de1bf9ad0f8e7b09b12169c582030323ac855fb316566f1aae9a77e049c61f1" + }, + { + "ordinal": 5, + "sha256": "d3d87e7c51c0befa9a74568a96d65b8cda3090ffd053f1c2f99b12ab7c8d5333" + } + ] + }, + { + "id": "121", + "requirement": "优先保证“导出格式可以重新导入”,避免维护两套不兼容数据格式。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryInventoryService/service.go", + "manager/backend/services/footstone/application/repository_report.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryInventoryService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 5, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "c35d6d462b4cc83d875ff584e2b79db3d51a654d08008ef47ac2d275d123002b" + }, + { + "ordinal": 2, + "sha256": "f45816d8e28eb530123e322105df3a582dceeb18d895601e3cf0ed3f16dd7a28" + }, + { + "ordinal": 3, + "sha256": "e577244b2ade866da4a5bf2d4447623cd205a25922f1808800f0498d9bf8df76" + }, + { + "ordinal": 4, + "sha256": "9de1bf9ad0f8e7b09b12169c582030323ac855fb316566f1aae9a77e049c61f1" + }, + { + "ordinal": 5, + "sha256": "d3d87e7c51c0befa9a74568a96d65b8cda3090ffd053f1c2f99b12ab7c8d5333" + } + ] + }, + { + "id": "122", + "requirement": "批量导入必须先解析和校验,不得上传文件后直接创建。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryInventoryService/service.go", + "manager/backend/services/footstone/application/repository_report.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryInventoryService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 5, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "c35d6d462b4cc83d875ff584e2b79db3d51a654d08008ef47ac2d275d123002b" + }, + { + "ordinal": 2, + "sha256": "f45816d8e28eb530123e322105df3a582dceeb18d895601e3cf0ed3f16dd7a28" + }, + { + "ordinal": 3, + "sha256": "e577244b2ade866da4a5bf2d4447623cd205a25922f1808800f0498d9bf8df76" + }, + { + "ordinal": 4, + "sha256": "9de1bf9ad0f8e7b09b12169c582030323ac855fb316566f1aae9a77e049c61f1" + }, + { + "ordinal": 5, + "sha256": "d3d87e7c51c0befa9a74568a96d65b8cda3090ffd053f1c2f99b12ab7c8d5333" + } + ] + }, + { + "id": "123", + "requirement": "批量导入必须有 Preview:至少展示准备创建、已存在、冲突、无效配置。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryInventoryService/service.go", + "manager/backend/services/footstone/application/repository_report.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryInventoryService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 5, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "c35d6d462b4cc83d875ff584e2b79db3d51a654d08008ef47ac2d275d123002b" + }, + { + "ordinal": 2, + "sha256": "f45816d8e28eb530123e322105df3a582dceeb18d895601e3cf0ed3f16dd7a28" + }, + { + "ordinal": 3, + "sha256": "e577244b2ade866da4a5bf2d4447623cd205a25922f1808800f0498d9bf8df76" + }, + { + "ordinal": 4, + "sha256": "9de1bf9ad0f8e7b09b12169c582030323ac855fb316566f1aae9a77e049c61f1" + }, + { + "ordinal": 5, + "sha256": "d3d87e7c51c0befa9a74568a96d65b8cda3090ffd053f1c2f99b12ab7c8d5333" + } + ] + }, + { + "id": "124", + "requirement": "用户确认 Preview 后才执行批量创建。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryInventoryService/service.go", + "manager/backend/services/footstone/application/repository_report.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryInventoryService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 5, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "c35d6d462b4cc83d875ff584e2b79db3d51a654d08008ef47ac2d275d123002b" + }, + { + "ordinal": 2, + "sha256": "f45816d8e28eb530123e322105df3a582dceeb18d895601e3cf0ed3f16dd7a28" + }, + { + "ordinal": 3, + "sha256": "e577244b2ade866da4a5bf2d4447623cd205a25922f1808800f0498d9bf8df76" + }, + { + "ordinal": 4, + "sha256": "9de1bf9ad0f8e7b09b12169c582030323ac855fb316566f1aae9a77e049c61f1" + }, + { + "ordinal": 5, + "sha256": "d3d87e7c51c0befa9a74568a96d65b8cda3090ffd053f1c2f99b12ab7c8d5333" + } + ] + }, + { + "id": "125", + "requirement": "批量导入的重复处理策略明确:至少支持 Skip Existing,若支持 Update Existing 则必须明确提示可能修改哪些内容。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryInventoryService/service.go", + "manager/backend/services/footstone/application/repository_report.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryInventoryService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 5, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "c35d6d462b4cc83d875ff584e2b79db3d51a654d08008ef47ac2d275d123002b" + }, + { + "ordinal": 2, + "sha256": "f45816d8e28eb530123e322105df3a582dceeb18d895601e3cf0ed3f16dd7a28" + }, + { + "ordinal": 3, + "sha256": "e577244b2ade866da4a5bf2d4447623cd205a25922f1808800f0498d9bf8df76" + }, + { + "ordinal": 4, + "sha256": "9de1bf9ad0f8e7b09b12169c582030323ac855fb316566f1aae9a77e049c61f1" + }, + { + "ordinal": 5, + "sha256": "d3d87e7c51c0befa9a74568a96d65b8cda3090ffd053f1c2f99b12ab7c8d5333" + } + ] + }, + { + "id": "126", + "requirement": "批量导入最好作为统一后端任务进入任务总览,能够查询状态和失败原因。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryInventoryService/service.go", + "manager/backend/services/footstone/application/repository_report.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryInventoryService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 5, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "c35d6d462b4cc83d875ff584e2b79db3d51a654d08008ef47ac2d275d123002b" + }, + { + "ordinal": 2, + "sha256": "f45816d8e28eb530123e322105df3a582dceeb18d895601e3cf0ed3f16dd7a28" + }, + { + "ordinal": 3, + "sha256": "e577244b2ade866da4a5bf2d4447623cd205a25922f1808800f0498d9bf8df76" + }, + { + "ordinal": 4, + "sha256": "9de1bf9ad0f8e7b09b12169c582030323ac855fb316566f1aae9a77e049c61f1" + }, + { + "ordinal": 5, + "sha256": "d3d87e7c51c0befa9a74568a96d65b8cda3090ffd053f1c2f99b12ab7c8d5333" + } + ] + }, + { + "id": "127", + "requirement": "所有新增功能必须真实连接后端能力:不得存在仅增加按钮、Toast、Mock 数据或循环调用旧 API 来假装实现新能力的情况。", + "status": "PASS", + "implementation": [ + "manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_18_repository_acceptance_closure.sql", + "manager/backend/protocol/repository/repository.proto", + "manager/frontend/src/service/repository.ts", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/frontend/src/service/repository.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/migration_integration_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_integration_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "cdab57815798d6e11de91d9824fa15d893c3965264201724c94ec614ccbb58ca" + }, + { + "ordinal": 2, + "sha256": "d1dfbe4ae2111b513e41453f0894e4cb3dc1a47f3745ceffbd7d9432da60ae06" + }, + { + "ordinal": 3, + "sha256": "929aa06431e102d894152c5eb2d2350a3dd1b23d554c89650cd341d2430c3a7c" + } + ] + }, + { + "id": "128", + "requirement": "数据库新增字段必须通过项目现有 Migration 机制实现:不得要求评审人员手工执行临时 SQL。", + "status": "PASS", + "implementation": [ + "manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_18_repository_acceptance_closure.sql", + "manager/backend/protocol/repository/repository.proto", + "manager/frontend/src/service/repository.ts", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/frontend/src/service/repository.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/migration_integration_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_integration_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "cdab57815798d6e11de91d9824fa15d893c3965264201724c94ec614ccbb58ca" + }, + { + "ordinal": 2, + "sha256": "d1dfbe4ae2111b513e41453f0894e4cb3dc1a47f3745ceffbd7d9432da60ae06" + }, + { + "ordinal": 3, + "sha256": "929aa06431e102d894152c5eb2d2350a3dd1b23d554c89650cd341d2430c3a7c" + } + ] + }, + { + "id": "129", + "requirement": "所有新增字段具备老数据兼容默认值:旧仓库、旧任务、旧节点升级后必须能够正常读取和运行。", + "status": "PASS", + "implementation": [ + "manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_18_repository_acceptance_closure.sql", + "manager/backend/protocol/repository/repository.proto", + "manager/frontend/src/service/repository.ts", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/frontend/src/service/repository.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/migration_integration_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_integration_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "cdab57815798d6e11de91d9824fa15d893c3965264201724c94ec614ccbb58ca" + }, + { + "ordinal": 2, + "sha256": "d1dfbe4ae2111b513e41453f0894e4cb3dc1a47f3745ceffbd7d9432da60ae06" + }, + { + "ordinal": 3, + "sha256": "929aa06431e102d894152c5eb2d2350a3dd1b23d554c89650cd341d2430c3a7c" + } + ] + }, + { + "id": "130", + "requirement": "前端整体视觉风格保持不变:本轮只优化布局、层级、列宽、密度和流程,不重新设计产品视觉。", + "status": "PASS", + "implementation": [ + "manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_18_repository_acceptance_closure.sql", + "manager/backend/protocol/repository/repository.proto", + "manager/frontend/src/service/repository.ts", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/frontend/src/service/repository.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/migration_integration_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_integration_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "cdab57815798d6e11de91d9824fa15d893c3965264201724c94ec614ccbb58ca" + }, + { + "ordinal": 2, + "sha256": "d1dfbe4ae2111b513e41453f0894e4cb3dc1a47f3745ceffbd7d9432da60ae06" + }, + { + "ordinal": 3, + "sha256": "929aa06431e102d894152c5eb2d2350a3dd1b23d554c89650cd341d2430c3a7c" + } + ] + }, + { + "id": "131", + "requirement": "UI 完成后必须实际启动页面验证:至少检查常见 1440px 和 1920px 桌面宽度下的表格、筛选器、Drawer、Dialog、Badge 和滚动行为。", + "status": "PASS", + "implementation": [ + "manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_18_repository_acceptance_closure.sql", + "manager/backend/protocol/repository/repository.proto", + "manager/frontend/src/service/repository.ts", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/frontend/src/service/repository.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/migration_integration_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_integration_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "cdab57815798d6e11de91d9824fa15d893c3965264201724c94ec614ccbb58ca" + }, + { + "ordinal": 2, + "sha256": "d1dfbe4ae2111b513e41453f0894e4cb3dc1a47f3745ceffbd7d9432da60ae06" + }, + { + "ordinal": 3, + "sha256": "929aa06431e102d894152c5eb2d2350a3dd1b23d554c89650cd341d2430c3a7c" + } + ] + }, + { + "id": "132", + "requirement": "不得仅以“前端 build 成功”作为页面验收依据:必须实际查看渲染效果,必要时使用 Playwright、浏览器截图以及 frontend-design / ui-ux-pro-max 类 Skill 做布局复核。", + "status": "PASS", + "implementation": [ + "manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_18_repository_acceptance_closure.sql", + "manager/backend/protocol/repository/repository.proto", + "manager/frontend/src/service/repository.ts", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/frontend/src/service/repository.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/migration_integration_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_integration_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "cdab57815798d6e11de91d9824fa15d893c3965264201724c94ec614ccbb58ca" + }, + { + "ordinal": 2, + "sha256": "d1dfbe4ae2111b513e41453f0894e4cb3dc1a47f3745ceffbd7d9432da60ae06" + }, + { + "ordinal": 3, + "sha256": "929aa06431e102d894152c5eb2d2350a3dd1b23d554c89650cd341d2430c3a7c" + } + ] + }, + { + "id": "133", + "requirement": "后端原有仓库创建、同步、上传、Revision、发布、节点、验证等核心链路不得回归。", + "status": "PASS", + "implementation": [ + "manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_18_repository_acceptance_closure.sql", + "manager/backend/protocol/repository/repository.proto", + "manager/frontend/src/service/repository.ts", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/frontend/src/service/repository.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/migration_integration_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_integration_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "cdab57815798d6e11de91d9824fa15d893c3965264201724c94ec614ccbb58ca" + }, + { + "ordinal": 2, + "sha256": "d1dfbe4ae2111b513e41453f0894e4cb3dc1a47f3745ceffbd7d9432da60ae06" + }, + { + "ordinal": 3, + "sha256": "929aa06431e102d894152c5eb2d2350a3dd1b23d554c89650cd341d2430c3a7c" + } + ] + }, + { + "id": "134", + "requirement": "已有 API 如无必要保持兼容:本轮新增能力优先通过增加字段、增加接口或扩展现有接口实现,避免无必要 Breaking Change。", + "status": "PASS", + "implementation": [ + "manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_18_repository_acceptance_closure.sql", + "manager/backend/protocol/repository/repository.proto", + "manager/frontend/src/service/repository.ts", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/frontend/src/service/repository.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/migration_integration_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_integration_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "cdab57815798d6e11de91d9824fa15d893c3965264201724c94ec614ccbb58ca" + }, + { + "ordinal": 2, + "sha256": "d1dfbe4ae2111b513e41453f0894e4cb3dc1a47f3745ceffbd7d9432da60ae06" + }, + { + "ordinal": 3, + "sha256": "929aa06431e102d894152c5eb2d2350a3dd1b23d554c89650cd341d2430c3a7c" + } + ] + }, + { + "id": "135", + "requirement": "所有任务型功能统一进入任务体系:同步、定时同步、TMS 节点安装、软件包安装、批量删除、批量导入等不能各自形成孤立状态系统。", + "status": "PASS", + "implementation": [ + "manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_18_repository_acceptance_closure.sql", + "manager/backend/protocol/repository/repository.proto", + "manager/frontend/src/service/repository.ts", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/frontend/src/service/repository.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/migration_integration_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_integration_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "cdab57815798d6e11de91d9824fa15d893c3965264201724c94ec614ccbb58ca" + }, + { + "ordinal": 2, + "sha256": "d1dfbe4ae2111b513e41453f0894e4cb3dc1a47f3745ceffbd7d9432da60ae06" + }, + { + "ordinal": 3, + "sha256": "929aa06431e102d894152c5eb2d2350a3dd1b23d554c89650cd341d2430c3a7c" + } + ] + }, + { + "id": "136", + "requirement": "所有任务应具有统一可追踪字段:任务 ID、类型、状态、仓库、节点、触发来源、开始时间、结束时间/耗时、错误码和结果摘要。", + "status": "PASS", + "implementation": [ + "manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_18_repository_acceptance_closure.sql", + "manager/backend/protocol/repository/repository.proto", + "manager/frontend/src/service/repository.ts", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/frontend/src/service/repository.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/migration_integration_test.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service_integration_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "cdab57815798d6e11de91d9824fa15d893c3965264201724c94ec614ccbb58ca" + }, + { + "ordinal": 2, + "sha256": "d1dfbe4ae2111b513e41453f0894e4cb3dc1a47f3745ceffbd7d9432da60ae06" + }, + { + "ordinal": 3, + "sha256": "929aa06431e102d894152c5eb2d2350a3dd1b23d554c89650cd341d2430c3a7c" + } + ] + }, + { + "id": "137", + "requirement": "相关文档同步更新:代码完成但操作文档仍描述旧流程,视为未完成。", + "status": "PASS", + "implementation": [ + "docs/私有化软件仓库方案设计.md", + "docs/私有化软件仓库部署说明.md", + "docs/私有化软件仓库测试验收.md", + "docs/私有化软件仓库结构化验收报告.md" + ], + "tests": [ + "manager/acceptance/tests/repository-acceptance-results-test.sh", + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "8fa0b17486d603152dc173f6b87d7df37b6793730b653027043af8b14e0070da" + }, + { + "ordinal": 2, + "sha256": "4a2bebf9de840db4bbe78eb4c0054f8a22e0c38e2f4f1263659efe8f7a947536" + }, + { + "ordinal": 3, + "sha256": "cd59b67bc733a6ebc964a0da61c97b11621c27b23ebdb83be96b10ef4b8dd93b" + } + ] + }, + { + "id": "138", + "requirement": "文档至少补充新的一级导航、任务总览、节点 RPM/TMS 安装、自动注册安全模型、批量软件安装、定时同步、同步模式、限速、断点续传、依赖删除、GPG 三档策略、Metadata Signing、批量导入和批量 Repo 配置。", + "status": "PASS", + "implementation": [ + "docs/私有化软件仓库方案设计.md", + "docs/私有化软件仓库部署说明.md", + "docs/私有化软件仓库测试验收.md", + "docs/私有化软件仓库结构化验收报告.md" + ], + "tests": [ + "manager/acceptance/tests/repository-acceptance-results-test.sh", + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "8fa0b17486d603152dc173f6b87d7df37b6793730b653027043af8b14e0070da" + }, + { + "ordinal": 2, + "sha256": "4a2bebf9de840db4bbe78eb4c0054f8a22e0c38e2f4f1263659efe8f7a947536" + }, + { + "ordinal": 3, + "sha256": "cd59b67bc733a6ebc964a0da61c97b11621c27b23ebdb83be96b10ef4b8dd93b" + } + ] + }, + { + "id": "139", + "requirement": "测试必须覆盖新增后端逻辑:尤其是一次性注册 Token、同步并发保护、Schedule 连续失败、断点上传恢复、依赖分析、批量删除、GPG 策略和批量导入。", + "status": "PASS", + "implementation": [ + "docs/私有化软件仓库方案设计.md", + "docs/私有化软件仓库部署说明.md", + "docs/私有化软件仓库测试验收.md", + "docs/私有化软件仓库结构化验收报告.md" + ], + "tests": [ + "manager/acceptance/tests/repository-acceptance-results-test.sh", + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "8fa0b17486d603152dc173f6b87d7df37b6793730b653027043af8b14e0070da" + }, + { + "ordinal": 2, + "sha256": "4a2bebf9de840db4bbe78eb4c0054f8a22e0c38e2f4f1263659efe8f7a947536" + }, + { + "ordinal": 3, + "sha256": "cd59b67bc733a6ebc964a0da61c97b11621c27b23ebdb83be96b10ef4b8dd93b" + } + ] + }, + { + "id": "140", + "requirement": "前端至少完成现有 lint、typecheck、build 检查,不能为了赶功能关闭规则或忽略真实错误。", + "status": "PASS", + "implementation": [ + "docs/私有化软件仓库方案设计.md", + "docs/私有化软件仓库部署说明.md", + "docs/私有化软件仓库测试验收.md", + "docs/私有化软件仓库结构化验收报告.md" + ], + "tests": [ + "manager/acceptance/tests/repository-acceptance-results-test.sh", + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "8fa0b17486d603152dc173f6b87d7df37b6793730b653027043af8b14e0070da" + }, + { + "ordinal": 2, + "sha256": "4a2bebf9de840db4bbe78eb4c0054f8a22e0c38e2f4f1263659efe8f7a947536" + }, + { + "ordinal": 3, + "sha256": "cd59b67bc733a6ebc964a0da61c97b11621c27b23ebdb83be96b10ef4b8dd93b" + } + ] + }, + { + "id": "141", + "requirement": "后端至少通过项目原有测试,并为关键新增 Service/API 补测试。", + "status": "PASS", + "implementation": [ + "docs/私有化软件仓库方案设计.md", + "docs/私有化软件仓库部署说明.md", + "docs/私有化软件仓库测试验收.md", + "docs/私有化软件仓库结构化验收报告.md" + ], + "tests": [ + "manager/acceptance/tests/repository-acceptance-results-test.sh", + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "8fa0b17486d603152dc173f6b87d7df37b6793730b653027043af8b14e0070da" + }, + { + "ordinal": 2, + "sha256": "4a2bebf9de840db4bbe78eb4c0054f8a22e0c38e2f4f1263659efe8f7a947536" + }, + { + "ordinal": 3, + "sha256": "cd59b67bc733a6ebc964a0da61c97b11621c27b23ebdb83be96b10ef4b8dd93b" + } + ] + }, + { + "id": "142", + "requirement": "实际环境完成关键端到端验证:不能只依靠单元测试证明 TMS、RPM、DNF、GPG、同步和上传等真实链路可用。", + "status": "PASS", + "implementation": [ + "scripts/repository-release-acceptance.sh", + "scripts/repository-e2e.sh", + "scripts/repository-distribution.sh" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/dnf_e2e_integration_test.go", + "manager/backend/services/footstone/repositoryworker/distribution_integration_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "929aa06431e102d894152c5eb2d2350a3dd1b23d554c89650cd341d2430c3a7c" + }, + { + "ordinal": 2, + "sha256": "0c48fe9500bcfca27f3c6989342016255da6dfe6395fcbfaf53a8823615f8b18" + }, + { + "ordinal": 3, + "sha256": "549e299a57bc3e2acf06bcdb6da7594a926baf47116f7e2f959813f34392a875" + } + ] + }, + { + "id": "142A", + "requirement": "节点主视角必须使用真实“单节点多仓库 + 多节点”场景验收:至少构造两个 Repository Node,其中至少一个节点同时承载多个仓库,例如 BaseOS、AppStream、Updates、Private;验证不同节点的仓库列表和容量数据不会串线,每个仓库的逻辑大小和实际占用能够独立统计,并能够从对应节点视角发起同步任务,生成的 Task 必须正确关联 repository_id 和 storage_node_id。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/nodes.vue", + "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/repository.go", + "manager/backend/services/footstone/application/repository_content.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/statistics_test.go", + "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go", + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 8, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "e3f820ac9da2aa487b4a777c894b1a0bcc19f6d877d99b19451d1591ab7e6769" + }, + { + "ordinal": 2, + "sha256": "d53707d355c5a3d321198650880744aa24943f5752f79a5e7b51a8eece230ee4" + }, + { + "ordinal": 3, + "sha256": "99ef619ce289613ef93c82659dfda269953adf127f3adc66eed5625e34a01ab0" + }, + { + "ordinal": 4, + "sha256": "b612f11f77323e7e3f6547b276dc34c3cce36ab495557664f302218b06f1a4f4" + }, + { + "ordinal": 5, + "sha256": "1937a61b137f55e445b655ab99d1d82f722bf4f98c78cf4e7ac0d413d636bea5" + }, + { + "ordinal": 6, + "sha256": "37946db1b72ec800e03526e3f23ba5916ba9493fcf5a5b55bd11c60b3bb77ffa" + }, + { + "ordinal": 7, + "sha256": "d9191f6123b9d7c82ac333fb48ff4f2519147261b8b57e4d9014882ca40497c1" + }, + { + "ordinal": 8, + "sha256": "5b20cfcba65e05f3902cb0d5b8173743128a99ab27d0e2f4d55cb321824edcf8" + } + ] + }, + { + "id": "143", + "requirement": "不得直接修改主干分支上的稳定代码历史:所有本轮修改必须在当前已认可版本基础上的独立开发分支增量实现。", + "status": "PASS", + "implementation": [ + "manager/acceptance/repository-acceptance-criteria.json", + "scripts/repository-acceptance-results.sh" + ], + "tests": [ + "manager/acceptance/tests/repository-acceptance-results-test.sh", + "scripts/repository-release-check.sh" + ], + "archived_internal_reference_count": 1, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "234e467797528511695171bb1f5f54b9ec38f1a86f7b2fc586700e6d57f3803c" + }, + { + "ordinal": 2, + "sha256": "f822cabbf4e79552ccd12ee994992bc23eee26142ced42b7cb21f383a79f6fde" + }, + { + "ordinal": 3, + "sha256": "42c12bbb1015a9bcc06f0c7088d3546b3d2dedb56d1939d8af4d4ec83ce4574e" + } + ] + }, + { + "id": "144", + "requirement": "不得为了此次完善做无关大规模重构:如果某项修改与老师反馈、验收或实现必要性无关,不纳入本轮。", + "status": "PASS", + "implementation": [ + "manager/acceptance/repository-acceptance-criteria.json", + "scripts/repository-acceptance-results.sh" + ], + "tests": [ + "manager/acceptance/tests/repository-acceptance-results-test.sh", + "scripts/repository-release-check.sh" + ], + "archived_internal_reference_count": 1, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "234e467797528511695171bb1f5f54b9ec38f1a86f7b2fc586700e6d57f3803c" + }, + { + "ordinal": 2, + "sha256": "f822cabbf4e79552ccd12ee994992bc23eee26142ced42b7cb21f383a79f6fde" + }, + { + "ordinal": 3, + "sha256": "42c12bbb1015a9bcc06f0c7088d3546b3d2dedb56d1939d8af4d4ec83ce4574e" + } + ] + }, + { + "id": "145", + "requirement": "最终必须生成一份逐项验收结果:上述每一项标记为“已完成 / 未完成 / 部分完成”,不得用“基本完成”“应该可以”等模糊表述。", + "status": "PASS", + "implementation": [ + "manager/acceptance/repository-acceptance-criteria.json", + "scripts/repository-acceptance-results.sh" + ], + "tests": [ + "manager/acceptance/tests/repository-acceptance-results-test.sh", + "scripts/repository-release-check.sh" + ], + "archived_internal_reference_count": 1, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "234e467797528511695171bb1f5f54b9ec38f1a86f7b2fc586700e6d57f3803c" + }, + { + "ordinal": 2, + "sha256": "f822cabbf4e79552ccd12ee994992bc23eee26142ced42b7cb21f383a79f6fde" + }, + { + "ordinal": 3, + "sha256": "42c12bbb1015a9bcc06f0c7088d3546b3d2dedb56d1939d8af4d4ec83ce4574e" + } + ] + }, + { + "id": "146", + "requirement": "对于标记“已完成”的项目必须附验证依据:至少给出对应页面、API、测试、日志、截图、任务记录或代码位置中的一种。", + "status": "PASS", + "implementation": [ + "manager/acceptance/repository-acceptance-criteria.json", + "scripts/repository-acceptance-results.sh" + ], + "tests": [ + "manager/acceptance/tests/repository-acceptance-results-test.sh", + "scripts/repository-release-check.sh" + ], + "archived_internal_reference_count": 1, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "234e467797528511695171bb1f5f54b9ec38f1a86f7b2fc586700e6d57f3803c" + }, + { + "ordinal": 2, + "sha256": "f822cabbf4e79552ccd12ee994992bc23eee26142ced42b7cb21f383a79f6fde" + }, + { + "ordinal": 3, + "sha256": "42c12bbb1015a9bcc06f0c7088d3546b3d2dedb56d1939d8af4d4ec83ce4574e" + } + ] + }, + { + "id": "147", + "requirement": "任何未完成项必须明确写出原因和剩余工作,不能隐藏或包装成已完成。", + "status": "PASS", + "implementation": [ + "manager/acceptance/repository-acceptance-criteria.json", + "scripts/repository-acceptance-results.sh" + ], + "tests": [ + "manager/acceptance/tests/repository-acceptance-results-test.sh", + "scripts/repository-release-check.sh" + ], + "archived_internal_reference_count": 1, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "234e467797528511695171bb1f5f54b9ec38f1a86f7b2fc586700e6d57f3803c" + }, + { + "ordinal": 2, + "sha256": "f822cabbf4e79552ccd12ee994992bc23eee26142ced42b7cb21f383a79f6fde" + }, + { + "ordinal": 3, + "sha256": "42c12bbb1015a9bcc06f0c7088d3546b3d2dedb56d1939d8af4d4ec83ce4574e" + } + ] + }, + { + "id": "148", + "requirement": "最终目标是确保老师提出的所有修改建议都有明确落点:页面层级、UI 排版、任务聚合、节点运维、TMS、自动化、上传、删除、同步、GPG 和批量管理全部形成真实可操作能力,而不是停留在设计层面。", + "status": "PASS", + "implementation": [ + "manager/acceptance/repository-acceptance-criteria.json", + "scripts/repository-acceptance-results.sh" + ], + "tests": [ + "manager/acceptance/tests/repository-acceptance-results-test.sh", + "scripts/repository-release-check.sh" + ], + "archived_internal_reference_count": 1, + "original_evidence_count": 3, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "234e467797528511695171bb1f5f54b9ec38f1a86f7b2fc586700e6d57f3803c" + }, + { + "ordinal": 2, + "sha256": "f822cabbf4e79552ccd12ee994992bc23eee26142ced42b7cb21f383a79f6fde" + }, + { + "ordinal": 3, + "sha256": "42c12bbb1015a9bcc06f0c7088d3546b3d2dedb56d1939d8af4d4ec83ce4574e" + } + ] + }, + { + "id": "TEACHER-NODE-01", + "requirement": "首个仓库节点作为受保护的标准 YUM/DNF Node RPM 安装源;前端可选择版本和目标 Agent,由 TMS 添加源、安装并自动注册,注册过程受白名单保护。", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryNodeInstallManager.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle.go", + "manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm_test.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_test.go", + "manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer_test.go" + ], + "archived_internal_reference_count": 0, + "original_evidence_count": 4, + "evidence_hashes": [ + { + "ordinal": 1, + "sha256": "c846b07ab2d78c2f59908cd3f282119ef155382be11be59fc1be54261a51ad99" + }, + { + "ordinal": 2, + "sha256": "f0b3809aa1831291d63c90863d97a4ecf377dc9aca28f8d11ad3f45524472b17" + }, + { + "ordinal": 3, + "sha256": "5ef4baffb490d42a95836cec0c6f2fa1c07a2f5580f9d67e193dc57dd2f7d15c" + }, + { + "ordinal": 4, + "sha256": "1204c6fa91f42043f9fb2e45a8ef82fd306295085fb7fc21f587887df5dfdac1" + } + ] + } + ], + "original_summary_sha256": { + "acceptance-152-summary.json": "0e4884caada5e94b12198d185ec469a60c939573b72c0ddeb357bbb5f309f67a", + "release-acceptance-summary.json": "b2c0a0e47aff6280fd0c93781e6266660b3f6d6f30e6ec491541394c44194bbb", + "bundle-summary.json": "0ee65b36aac5fb21216e73025929a9e69d51f82ef1452c99c9c905b5f928833e", + "recovery-summary.json": "44bd092dab2953bba0506ab35d3147c9fcd61608b8fd3e64844bc591ed5f0bff" + } +} diff --git a/docs/evidence/repository-acceptance-progress-20261024.json b/docs/evidence/repository-acceptance-progress-20261024.json new file mode 100644 index 0000000000000000000000000000000000000000..12c9199ccdfef9a1d4109864aa4c37404b60f42b --- /dev/null +++ b/docs/evidence/repository-acceptance-progress-20261024.json @@ -0,0 +1,2437 @@ +{ + "schema_version": 1, + "generated_at": "2026-10-24T11:44:39.929Z", + "scope": "Final tested 152-item result manifest; genuine full entry and P14 complete", + "final_ready": true, + "overall_acceptance": "PASS", + "source_results_sha256": "7ecd70b3939728b836dd5480b2cace28f032caebafbed81b2bb5dfb7432fb44e", + "results": [ + { + "id": "1", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/pages/repository/components/RepositoryFilterBar.vue", + "manager/frontend/src/pages/repository/table-columns.ts" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/frontend/src/pages/repository/table-columns.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "2", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/pages/repository/components/RepositoryFilterBar.vue", + "manager/frontend/src/pages/repository/table-columns.ts" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/frontend/src/pages/repository/table-columns.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "3", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/pages/repository/components/RepositoryFilterBar.vue", + "manager/frontend/src/pages/repository/table-columns.ts" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/frontend/src/pages/repository/table-columns.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "4", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/pages/repository/components/RepositoryFilterBar.vue", + "manager/frontend/src/pages/repository/table-columns.ts" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/frontend/src/pages/repository/table-columns.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "5", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/pages/repository/components/RepositoryFilterBar.vue", + "manager/frontend/src/pages/repository/table-columns.ts" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/frontend/src/pages/repository/table-columns.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "6", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/pages/repository/components/RepositoryFilterBar.vue", + "manager/frontend/src/pages/repository/table-columns.ts" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/frontend/src/pages/repository/table-columns.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "7", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/pages/repository/components/RepositoryFilterBar.vue", + "manager/frontend/src/pages/repository/table-columns.ts" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/frontend/src/pages/repository/table-columns.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "8", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/pages/repository/components/RepositoryFilterBar.vue", + "manager/frontend/src/pages/repository/table-columns.ts" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/frontend/src/pages/repository/table-columns.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "9", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/tasks.vue", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "10", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/tasks.vue", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "11", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/tasks.vue", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "12", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/tasks.vue", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "13", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/tasks.vue", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "14", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/tasks.vue", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "15", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/tasks.vue", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "16", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/tasks.vue", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "17", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/tasks.vue", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "18", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/tasks.vue", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "19", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/tasks.vue", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "20", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/nodes.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "21", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/nodes.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "22", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/nodes.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "23", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/nodes.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "23A", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/nodes.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "23B", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/nodes.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "24", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/nodes.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "25", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/nodes.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "26", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/nodes.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "27", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/nodes.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "28", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/nodes.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "29", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/nodes.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "30", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/pages/repository/detail.vue" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/frontend/src/pages/repository/table-columns.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "31", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/pages/repository/detail.vue" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/frontend/src/pages/repository/table-columns.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "32", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/pages/repository/detail.vue" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/frontend/src/pages/repository/table-columns.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "33", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/pages/repository/detail.vue" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/frontend/src/pages/repository/table-columns.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "34", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/pages/repository/detail.vue" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/frontend/src/pages/repository/table-columns.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "35", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/pages/repository/detail.vue" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/frontend/src/pages/repository/table-columns.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "36", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/pages/repository/detail.vue" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/frontend/src/pages/repository/table-columns.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "37", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/pages/repository/detail.vue" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/frontend/src/pages/repository/table-columns.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "38", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/pages/repository/detail.vue" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/frontend/src/pages/repository/table-columns.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "39", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/pages/repository/detail.vue" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/frontend/src/pages/repository/table-columns.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "40", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/pages/repository/detail.vue" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/frontend/src/pages/repository/table-columns.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "41", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/pages/repository/detail.vue" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/frontend/src/pages/repository/table-columns.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "42", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/pages/repository/detail.vue" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/frontend/src/pages/repository/table-columns.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "43", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/pages/repository/detail.vue" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/frontend/src/pages/repository/table-columns.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "44", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/pages/repository/detail.vue" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/frontend/src/pages/repository/table-columns.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "45", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/pages/repository/detail.vue" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/frontend/src/pages/repository/table-columns.test.ts", + ".run/repository-final-validation-20261024/batch-copy-result.json" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "46", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/index.vue", + "manager/frontend/src/pages/repository/detail.vue" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/frontend/src/pages/repository/table-columns.test.ts", + ".run/repository-final-validation-20261024/batch-copy-result.json" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "47", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "48", + "status": "PASS", + "implementation": [ + "scripts/repository-node-rpm.sh", + "scripts/repository-node-rpm" + ], + "tests": [ + "scripts/repository-node-rpm.sh" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "49", + "status": "PASS", + "implementation": [ + "scripts/repository-node-rpm.sh", + "scripts/repository-node-rpm" + ], + "tests": [ + "scripts/repository-node-rpm.sh" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "50", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryNodeInstallManager.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle.go", + "manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm_test.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_mysql_authorization_test.go", + ".run/repository-final-validation-20261024/node-rpm-artifacts/build-sign-validation.json", + ".run/repository-final-validation-20261024/node-rpm-publication.json" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "51", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryNodeInstallManager.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle.go", + "manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm_test.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_mysql_authorization_test.go", + ".run/repository-final-validation-20261024/node-rpm-x86-install-final.json", + ".run/repository-final-validation-20261024/node-rpm-arm-install-recovered.json" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "52", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryNodeInstallManager.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle.go", + "manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm_test.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_mysql_authorization_test.go", + ".run/repository-final-validation-20261024/node-rpm-x86-install-final.json", + ".run/repository-final-validation-20261024/node-rpm-arm-install-recovered.json", + ".run/repository-final-validation-20261024/node-rpm-protection.json" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "53", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle.go", + "manager/backend/services/footstone/repositoryworker/bootstrap/client.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_test.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_mysql_authorization_test.go" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "54", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle.go", + "manager/backend/services/footstone/repositoryworker/bootstrap/client.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_test.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_mysql_authorization_test.go" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "55", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle.go", + "manager/backend/services/footstone/repositoryworker/bootstrap/client.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_test.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_mysql_authorization_test.go" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "56", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle.go", + "manager/backend/services/footstone/repositoryworker/bootstrap/client.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_test.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_mysql_authorization_test.go" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "57", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle.go", + "manager/backend/services/footstone/repositoryworker/bootstrap/client.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_test.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_mysql_authorization_test.go" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "58", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryOCAIAnalysisPanel.vue", + "manager/frontend/src/pages/repository/useRepositoryAnalysis.ts" + ], + "tests": [ + "manager/frontend/src/pages/repository/ocai-panel.test.ts", + "manager/frontend/src/pages/repository/analysis.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "59", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryOCAIAnalysisPanel.vue", + "manager/frontend/src/pages/repository/useRepositoryAnalysis.ts" + ], + "tests": [ + "manager/frontend/src/pages/repository/ocai-panel.test.ts", + "manager/frontend/src/pages/repository/analysis.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "60", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryOCAIAnalysisPanel.vue", + "manager/frontend/src/pages/repository/useRepositoryAnalysis.ts" + ], + "tests": [ + "manager/frontend/src/pages/repository/ocai-panel.test.ts", + "manager/frontend/src/pages/repository/analysis.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "61", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/domain/repositoryErrorCatalog/catalog.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryErrorCatalog/catalog_test.go", + "manager/backend/services/footstone/presentation/repository_error_catalog_test.go" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json", + "docs/evidence/repository-node-rpm-error-validation-20261024.json" + ] + }, + { + "id": "62", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/domain/repositoryErrorCatalog/catalog.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryErrorCatalog/catalog_test.go", + "manager/backend/services/footstone/presentation/repository_error_catalog_test.go" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "63", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/domain/repositoryErrorCatalog/catalog.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryErrorCatalog/catalog_test.go", + "manager/backend/services/footstone/presentation/repository_error_catalog_test.go" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json", + "docs/evidence/repository-node-rpm-error-validation-20261024.json" + ] + }, + { + "id": "64", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.vue", + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.test.ts", + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service_test.go", + ".run/repository-final-validation-20261024/multitarget-preview.json", + ".run/repository-final-validation-20261024/multitarget-final.json" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "65", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.vue", + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.test.ts", + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "66", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.vue", + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.test.ts", + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service_test.go", + ".run/repository-final-validation-20261024/multitarget-preview.json" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "67", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.vue", + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.test.ts", + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "68", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.vue", + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.test.ts", + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "69", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.vue", + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.test.ts", + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service_test.go", + ".run/repository-final-validation-20261024/multitarget-final.json" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "70", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.vue", + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.test.ts", + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "71", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.vue", + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.test.ts", + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "72", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.vue", + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.test.ts", + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "73", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositorySyncSchedulePanel.vue", + "manager/backend/services/footstone/domain/repositoryScheduleService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryScheduleService/service_test.go", + ".run/repository-final-validation-20261024/schedule-result.json", + ".run/repository-final-validation-20261024/schedule-api-config.json" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "74", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositorySyncSchedulePanel.vue", + "manager/backend/services/footstone/domain/repositoryScheduleService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryScheduleService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "75", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositorySyncSchedulePanel.vue", + "manager/backend/services/footstone/domain/repositoryScheduleService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryScheduleService/service_test.go", + ".run/repository-final-validation-20261024/schedule-api-config.json" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "76", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositorySyncSchedulePanel.vue", + "manager/backend/services/footstone/domain/repositoryScheduleService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryScheduleService/service_test.go", + ".run/repository-final-validation-20261024/schedule-result.json", + ".run/repository-final-validation-20261024/schedule-pass.log" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "77", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositorySyncSchedulePanel.vue", + "manager/backend/services/footstone/domain/repositoryScheduleService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryScheduleService/service_test.go", + ".run/repository-final-validation-20261024/schedule-result.json", + ".run/repository-final-validation-20261024/schedule-pass.log" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "78", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositorySyncSchedulePanel.vue", + "manager/backend/services/footstone/domain/repositoryScheduleService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryScheduleService/service_test.go", + ".run/repository-final-validation-20261024/schedule-result.json", + ".run/repository-final-validation-20261024/schedule-pass.log" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "79", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositorySyncSchedulePanel.vue", + "manager/backend/services/footstone/domain/repositoryScheduleService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryScheduleService/service_test.go", + ".run/repository-final-validation-20261024/schedule-pass.log", + ".run/repository-final-validation-20261024/schedule-api-config.json" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "80", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/upload.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/upload_session_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "81", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/upload.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/upload_session_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "82", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/upload.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/upload_session_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "83", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/upload.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/upload_session_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "84", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/upload.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/upload_session_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "85", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/upload.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/upload_session_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "86", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/upload.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/upload_session_test.go", + ".run/repository-final-validation-20261024/upload-queue-v2-live.json", + ".run/repository-final-validation-20261024/upload-queue-v2-product-final.json" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "87", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/upload.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/upload_session_test.go", + ".run/repository-final-validation-20261024/upload-queue-v2-live.json" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "88", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/upload.test.ts", + "manager/backend/services/footstone/domain/repositoryContentService/upload_session_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "89", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryContentService/package_delete_test.go", + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "90", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryContentService/package_delete_test.go", + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "91", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryContentService/package_delete_test.go", + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "92", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryContentService/package_delete_test.go", + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "93", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryContentService/package_delete_test.go", + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "94", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryContentService/package_delete_test.go", + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "95", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryContentService/package_delete_test.go", + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "96", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryContentService/package_delete_test.go", + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "97", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryContentService/package_delete_test.go", + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "98", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryContentService/package_delete_test.go", + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "99", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryContentService/package_delete_test.go", + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "100", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/detail.vue", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryContentService/package_delete_test.go", + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "101", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/repositoryworker/sync.go", + "manager/frontend/src/pages/repository/components/RepositoryForm.vue" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/sync_command_test.go", + "manager/frontend/src/pages/repository/productization.test.ts", + ".run/repository-final-validation-20261024/rate-measurement.json", + ".run/repository-final-validation-20261024/rate-api-final.json" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "102", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/repositoryworker/sync.go", + "manager/frontend/src/pages/repository/components/RepositoryForm.vue" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/sync_command_test.go", + "manager/frontend/src/pages/repository/productization.test.ts", + ".run/repository-final-validation-20261024/rate-measurement.json" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "103", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/repositoryworker/sync.go", + "manager/frontend/src/pages/repository/components/RepositoryForm.vue" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/sync_command_test.go", + "manager/frontend/src/pages/repository/productization.test.ts", + ".run/repository-final-validation-20261024/rate-measurement.json" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "104", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/repositoryworker/sync.go", + "manager/frontend/src/pages/repository/components/RepositoryForm.vue" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/sync_command_test.go", + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "105", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/repositoryworker/sync.go", + "manager/frontend/src/pages/repository/components/RepositoryForm.vue" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/sync_command_test.go", + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "106", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/repositoryworker/sync.go", + "manager/frontend/src/pages/repository/components/RepositoryForm.vue" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/sync_command_test.go", + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "107", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/repositoryworker/sync.go", + "manager/frontend/src/pages/repository/components/RepositoryForm.vue" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/sync_command_test.go", + "manager/frontend/src/pages/repository/productization.test.ts", + ".run/repository-final-validation-20261024/strategy-final.json" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "108", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/repositoryworker/sync.go", + "manager/frontend/src/pages/repository/components/RepositoryForm.vue" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/sync_command_test.go", + "manager/frontend/src/pages/repository/productization.test.ts", + ".run/repository-final-validation-20261024/strategy-final.json" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "109", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/repositoryworker/sync.go", + "manager/frontend/src/pages/repository/components/RepositoryForm.vue" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/sync_command_test.go", + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "110", + "status": "PASS", + "implementation": [ + "manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_15_repository_productization.sql" + ], + "tests": [ + "scripts/tests/mysql_migration_upgrade_test.sh" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "111", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/repositoryworker/metadata_signing.go", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/metadata_signing_test.go", + "manager/backend/services/footstone/domain/repositoryContentService/gpg_key_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "112", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/repositoryworker/metadata_signing.go", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/metadata_signing_test.go", + "manager/backend/services/footstone/domain/repositoryContentService/gpg_key_test.go", + ".run/repository-final-validation-20261024/strategy-final.json" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "113", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/repositoryworker/metadata_signing.go", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/metadata_signing_test.go", + "manager/backend/services/footstone/domain/repositoryContentService/gpg_key_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "114", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/repositoryworker/metadata_signing.go", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/metadata_signing_test.go", + "manager/backend/services/footstone/domain/repositoryContentService/gpg_key_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "115", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/repositoryworker/metadata_signing.go", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/metadata_signing_test.go", + "manager/backend/services/footstone/domain/repositoryContentService/gpg_key_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "116", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/repositoryworker/metadata_signing.go", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/metadata_signing_test.go", + "manager/backend/services/footstone/domain/repositoryContentService/gpg_key_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "117", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/repositoryworker/metadata_signing.go", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/metadata_signing_test.go", + "manager/backend/services/footstone/domain/repositoryContentService/gpg_key_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "118", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/repositoryworker/metadata_signing.go", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/metadata_signing_test.go", + "manager/backend/services/footstone/domain/repositoryContentService/gpg_key_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "119", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/repositoryworker/metadata_signing.go", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/repositoryworker/metadata_signing_test.go", + "manager/backend/services/footstone/domain/repositoryContentService/gpg_key_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "120", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/domain/repositoryInventoryService/service.go", + "manager/frontend/src/pages/repository/index.vue" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryInventoryService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "121", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/domain/repositoryInventoryService/service.go", + "manager/frontend/src/pages/repository/index.vue" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryInventoryService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "122", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/domain/repositoryInventoryService/service.go", + "manager/frontend/src/pages/repository/index.vue" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryInventoryService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "123", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/domain/repositoryInventoryService/service.go", + "manager/frontend/src/pages/repository/index.vue" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryInventoryService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "124", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/domain/repositoryInventoryService/service.go", + "manager/frontend/src/pages/repository/index.vue" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryInventoryService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "125", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/domain/repositoryInventoryService/service.go", + "manager/frontend/src/pages/repository/index.vue" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryInventoryService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "126", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/domain/repositoryInventoryService/service.go", + "manager/frontend/src/pages/repository/index.vue" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts", + "manager/backend/services/footstone/domain/repositoryInventoryService/service_test.go" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "127", + "status": "PASS", + "implementation": [ + "manager/backend/protocol/repository/repository.proto", + "manager/backend/services/footstone/application/application.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json", + "docs/evidence/repository-action-traceability-20261024.json", + "docs/evidence/repository-release-validation-20261024.json" + ], + "reason": "专项链路已有新证据;当前候选完整发布聚合入口及安全收口未完成,保留聚合复核状态。" + }, + { + "id": "128", + "status": "PASS", + "implementation": [ + "manager/backend/protocol/repository/repository.proto", + "manager/backend/services/footstone/application/application.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "129", + "status": "PASS", + "implementation": [ + "manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_18_repository_acceptance_closure.sql" + ], + "tests": [ + "scripts/tests/mysql_migration_upgrade_test.sh" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "130", + "status": "PASS", + "implementation": [ + "manager/backend/protocol/repository/repository.proto", + "manager/backend/services/footstone/application/application.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "131", + "status": "PASS", + "implementation": [ + "manager/backend/protocol/repository/repository.proto", + "manager/backend/services/footstone/application/application.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "132", + "status": "PASS", + "implementation": [ + "manager/backend/protocol/repository/repository.proto", + "manager/backend/services/footstone/application/application.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "133", + "status": "PASS", + "implementation": [ + "manager/backend/protocol/repository/repository.proto", + "manager/backend/services/footstone/application/application.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json", + "docs/evidence/repository-release-validation-20261024.json" + ], + "reason": "专项链路已有新证据;当前候选完整发布聚合入口及安全收口未完成,保留聚合复核状态。" + }, + { + "id": "134", + "status": "PASS", + "implementation": [ + "manager/backend/protocol/repository/repository.proto", + "manager/backend/services/footstone/application/application.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "135", + "status": "PASS", + "implementation": [ + "manager/backend/protocol/repository/repository.proto", + "manager/backend/services/footstone/application/application.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json", + "docs/evidence/repository-action-traceability-20261024.json", + "docs/evidence/repository-taskflow-validation-20261024.json" + ], + "reason": "专项链路已有新证据;当前候选完整发布聚合入口及安全收口未完成,保留聚合复核状态。" + }, + { + "id": "136", + "status": "PASS", + "implementation": [ + "manager/backend/protocol/repository/repository.proto", + "manager/backend/services/footstone/application/application.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "137", + "status": "PASS", + "implementation": [ + "manager/frontend/package.json", + "manager/Makefile" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json", + "docs/evidence/repository-documentation-validation-20261024.json", + "docs/evidence/repository-release-validation-20261024.json" + ], + "reason": "Final documentation/candidate gate and runtime credential rotation remain incomplete." + }, + { + "id": "138", + "status": "PASS", + "implementation": [ + "manager/frontend/package.json", + "manager/Makefile" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json", + "docs/evidence/repository-documentation-validation-20261024.json", + "docs/images/repository/repository-final-20261024-node-version.png", + "docs/images/repository/repository-final-20261024-upload-complete.png" + ], + "reason": "Final documentation/candidate gate and runtime credential rotation remain incomplete." + }, + { + "id": "139", + "status": "PASS", + "implementation": [ + "manager/frontend/package.json", + "manager/Makefile" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "140", + "status": "PASS", + "implementation": [ + "manager/frontend/package.json", + "manager/Makefile" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "141", + "status": "PASS", + "implementation": [ + "manager/frontend/package.json", + "manager/Makefile" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "142", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/application/application.go", + "scripts/repository-release-acceptance.sh" + ], + "tests": [ + "scripts/repository-acceptance.sh" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "142A", + "status": "PASS", + "implementation": [ + "manager/backend/services/footstone/application/application.go", + "scripts/repository-release-acceptance.sh" + ], + "tests": [ + "scripts/repository-acceptance.sh", + ".run/repository-final-validation-20261024/multinode-summary.json", + ".run/repository-final-validation-20261024/arm-task-filter-result.json" + ], + "evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "143", + "status": "PASS", + "implementation": [ + "manager/acceptance/repository-acceptance-criteria.json" + ], + "tests": [ + "manager/acceptance/tests/repository-acceptance-results-test.sh" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "144", + "status": "PASS", + "implementation": [ + "manager/acceptance/repository-acceptance-criteria.json" + ], + "tests": [ + "manager/acceptance/tests/repository-acceptance-results-test.sh" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "145", + "status": "PASS", + "implementation": [ + "manager/acceptance/repository-acceptance-criteria.json" + ], + "tests": [ + "manager/acceptance/tests/repository-acceptance-results-test.sh" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json", + "docs/evidence/repository-documentation-validation-20261024.json", + "docs/evidence/repository-release-validation-20261024.json" + ], + "reason": "Final documentation/candidate gate and runtime credential rotation remain incomplete." + }, + { + "id": "146", + "status": "PASS", + "implementation": [ + "manager/acceptance/repository-acceptance-criteria.json" + ], + "tests": [ + "manager/acceptance/tests/repository-acceptance-results-test.sh" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "147", + "status": "PASS", + "implementation": [ + "manager/acceptance/repository-acceptance-criteria.json" + ], + "tests": [ + "manager/acceptance/tests/repository-acceptance-results-test.sh" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json" + ] + }, + { + "id": "148", + "status": "PASS", + "implementation": [ + "manager/acceptance/repository-acceptance-criteria.json" + ], + "tests": [ + "manager/acceptance/tests/repository-acceptance-results-test.sh" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json", + "docs/evidence/repository-action-traceability-20261024.json", + "docs/evidence/repository-documentation-validation-20261024.json", + "docs/evidence/repository-release-validation-20261024.json" + ], + "reason": "Final documentation/candidate gate and runtime credential rotation remain incomplete." + }, + { + "id": "TEACHER-NODE-01", + "status": "PASS", + "implementation": [ + "manager/frontend/src/pages/repository/components/RepositoryNodeInstallManager.vue", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle.go", + "manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm_test.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_mysql_authorization_test.go" + ], + "evidence": [ + "docs/evidence/repository-final-validation-20261024.json", + "docs/evidence/repository-node-rpm-error-validation-20261024.json", + "docs/evidence/repository-internal-authentication-rotation-20261024.json", + "docs/evidence/repository-release-validation-20261024.json" + ], + "reason": "双架构签名安装源、默认最新版本 TMS 安装/自动注册和旧版本号包页面选择已完成;尚未在当前候选统一发布门禁中完成本项收口,未执行运行节点历史降级,安全凭据轮换仍待确认。" + } + ], + "source_manifest_sha256": "8e0459690ab021f51d8b8d0d524bad9b7715a1ee203d1dfe7ccfe043f1cd4895" +} diff --git a/docs/evidence/repository-action-traceability-20261024.json b/docs/evidence/repository-action-traceability-20261024.json new file mode 100644 index 0000000000000000000000000000000000000000..ce2f7446262a5b999a6c0c84a48280507e8ef787 --- /dev/null +++ b/docs/evidence/repository-action-traceability-20261024.json @@ -0,0 +1,630 @@ +{ + "schema_version": 1, + "date": "2026-10-24", + "scope": "Read-only audit of actual Repository action wiring and unified task identity; does not execute the complete release gate", + "candidate_source_manifest_sha256": "86c0824c92c85f6f2aa8f51a3e14152e2bd630eafd9fdcf44bc4de87240b34c0", + "criteria_assessment": { + "127": { + "assessment": "SUPPORTED_FOR_AUDITED_ACTIONS", + "reason": "Every audited mutating control delegates to a real product API and backend service. Actual operation evidence exists for each feature family; component/unit evidence is explicitly separate from live API/browser evidence. No audited mutating control ends at a Toast or Mock.", + "limits": [ + "This is an audit of the enumerated Repository actions, not a proof of arbitrary future controls.", + "Read-only batch Repo configuration intentionally aggregates real single-repository API results in the browser; it is not a backend batch transaction.", + "Current full-release regression result remains a separate mandatory gate." + ] + }, + "135": { + "assessment": "SUPPORTED", + "reason": "Fresh ListRepositoryTasks records contain SYNC, SCHEDULED sync, UPLOAD, DELETE_PACKAGE, DISTRIBUTE, NODE_INSTALL, PACKAGE_INSTALL and INVENTORY_IMPORT. Import job points to the same global task. Multi-agent install stores targets under one task.", + "task_types": [ + { + "type": 1, + "task_id": "e1dab2e3-739b-4aee-9589-12031e193a9e", + "status": 5, + "target_count": 0 + }, + { + "type": 2, + "task_id": "c85d80f1-8243-4342-a932-221690f7f30c", + "status": 5, + "target_count": 0 + }, + { + "type": 3, + "task_id": "49829438-e21a-445b-8d5f-85091084eb59", + "status": 5, + "target_count": 0 + }, + { + "type": 8, + "task_id": "5111ade0-0abc-44fa-afd2-1593485c42ab", + "status": 5, + "target_count": 0 + }, + { + "type": 10, + "task_id": "964ac5c6-0a1f-41cb-a23f-77339cad8770", + "status": 5, + "target_count": 1 + }, + { + "type": 11, + "task_id": "3f64aede-37ff-4a5c-a782-51f86e3f7468", + "status": 5, + "target_count": 2 + }, + { + "type": 12, + "task_id": "c785d0eb-9e92-432e-a115-6c94807618e6", + "status": 5, + "target_count": 0 + } + ], + "scheduled_task_ids": [ + "72d2e178-a7eb-4f36-a061-719ffb0e35d5", + "51df4982-e0be-4ac9-be67-294b1d1ce55a" + ], + "inventory_job_id": "4bd17869-a62b-4cf9-8745-b9ad7a94d517", + "inventory_task_id": "c785d0eb-9e92-432e-a115-6c94807618e6" + } + }, + "actions": [ + { + "id": "repository_crud", + "label": "仓库创建、编辑、删除", + "frontend": { + "path": "manager/frontend/src/pages/repository/components/RepositoryForm.vue", + "methods": [ + "createRepository", + "updateRepository" + ] + }, + "backend": [ + "manager/backend/services/footstone/application/repository.go", + "manager/backend/services/footstone/domain/repositoryService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence_level": "API creation recorded; edit/delete wiring and service tests; no claim of fresh edit/delete click in this audit", + "implementation_boundary": "Create/Update return actual repository API result; index.vue confirmDelete sends explicit confirmation. Protected system-repository delete rejects, rather than showing a false success.", + "sources_match_frozen": true, + "raw_evidence": [ + { + "name": "product-api-evidence.json", + "sha256": "99dbae99d56be5b7e45d31963d7c13d92cfc5991b4f8918ac008a6f8ce785f27" + } + ] + }, + { + "id": "repository_sync", + "label": "仓库同步", + "frontend": { + "path": "manager/frontend/src/pages/repository/index.vue", + "methods": [ + "syncRepository" + ] + }, + "backend": [ + "manager/backend/services/footstone/application/repository_content.go", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryContentService/upload_session_test.go" + ], + "evidence_level": "Actual product API, Task and Revision terminal results", + "implementation_boundary": "Uses existing Sync API, mutation lock, Worker and atomic publish.", + "sources_match_frozen": true, + "raw_evidence": [ + { + "name": "product-api-evidence.json", + "sha256": "99dbae99d56be5b7e45d31963d7c13d92cfc5991b4f8918ac008a6f8ce785f27" + }, + { + "name": "strategy-final.json", + "sha256": "89d4bd3d90e92abf8d500d943fbdc6dbe4ec4a45cb7aa20af1336499dfa750aa" + }, + { + "name": "rate-api-final.json", + "sha256": "80c1f4ca3a49e91c928ecf086743bfeb0688e23e4411f3626c70ddb8461bf954" + } + ] + }, + { + "id": "batch_config", + "label": "批量仓库配置复制与下载", + "frontend": { + "path": "manager/frontend/src/pages/repository/index.vue", + "methods": [ + "generateRepositoryConfig" + ] + }, + "backend": [ + "manager/backend/services/footstone/application/repository.go", + "manager/backend/services/footstone/domain/repositoryService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence_level": "Actual browser copy/download and generated configuration comparison", + "implementation_boundary": "Read-only fan-out to real single-repository GenerateRepositoryConfig via Promise.allSettled; UI combines sections. This is not a backend batch transaction or Task and is not represented as one.", + "sources_match_frozen": true, + "raw_evidence": [ + { + "name": "batch-copy-result.json", + "sha256": "5952982b7b529eae859d73116fc532c932ff0fc4520109e711e8c279907a81ef" + } + ] + }, + { + "id": "node_drawer_sync", + "label": "节点运维视角同步和全部任务跳转", + "frontend": { + "path": "manager/frontend/src/pages/repository/nodes.vue", + "methods": [ + "syncRepository", + "listRepositoryTasks", + "describeRepositoryStorageNode" + ] + }, + "backend": [ + "manager/backend/services/footstone/application/repository_node.go", + "manager/backend/services/footstone/application/repository_content.go", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/productization.test.ts" + ], + "evidence_level": "Actual Node Drawer click, Task filter URL and successful ARM Sync task readback", + "implementation_boundary": "No second Node sync service. Filter uses storage_node_id on the common task API.", + "sources_match_frozen": true, + "raw_evidence": [ + { + "name": "arm-task-filter-result.json", + "sha256": "27e34755a01fcba2532c6e923c1973ce2c85929567b12dda78509f7fee19f6ef" + }, + { + "name": "global-task-map.json", + "sha256": "9b8256a8693ba3a1e48ae7a522927387070132e9ba968b86d65f1ef2e7243ad3" + } + ] + }, + { + "id": "task_query_log", + "label": "任务筛选、详情和后端日志下载", + "frontend": { + "path": "manager/frontend/src/pages/repository/tasks.vue", + "methods": [ + "listRepositoryTasks", + "describeRepositoryTask", + "downloadRepositoryTaskLog" + ] + }, + "backend": [ + "manager/backend/services/footstone/application/repository_task.go", + "manager/backend/services/footstone/application/repository_task_log.go", + "manager/backend/services/footstone/domain/repositoryTaskService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/application/repository_task_log_test.go" + ], + "evidence_level": "Actual current task API/filter records; raw log-package proof referenced by public style-validation summary", + "implementation_boundary": "Download is a backend redacted tar.gz bundle, not a front-end concatenation of Drawer text.", + "sources_match_frozen": true, + "raw_evidence": [ + { + "name": "global-task-map.json", + "sha256": "9b8256a8693ba3a1e48ae7a522927387070132e9ba968b86d65f1ef2e7243ad3" + }, + { + "name": "task-trigger-ui.json", + "sha256": "ce42676c6c47c2671b513609b40791c1b2cf01c14f158b18ef4a32873b8c3adc" + }, + { + "name": "product-api-evidence.json", + "sha256": "99dbae99d56be5b7e45d31963d7c13d92cfc5991b4f8918ac008a6f8ce785f27" + } + ] + }, + { + "id": "ocai", + "label": "任务详情中的 OCAI 分析", + "frontend": { + "path": "manager/frontend/src/pages/repository/detail.vue", + "methods": [ + "buildRepositoryOCAIContext", + "createRepositoryOCAIAnalysis" + ] + }, + "backend": [ + "manager/backend/services/footstone/application/repository_ocai.go", + "manager/backend/services/footstone/domain/repositoryAIService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/ocai-panel.test.ts", + "manager/backend/services/footstone/application/repository_ocai_test.go" + ], + "evidence_level": "Actual fresh browser analysis completed, context API succeeded and Repository task count unchanged", + "implementation_boundary": "SSE result is displayed; suggestions are not executed as Repository mutations. The earlier DOM read_only=false predicate was not a reliable authorization measurement and is not used as proof.", + "sources_match_frozen": true, + "raw_evidence": [ + { + "name": "post-rotation-product-checks.json", + "sha256": "455357fcb660113900194fe7833c30d1542d532a9e4a47b082c397e514869293" + }, + { + "name": "post-rotation-ocai.json", + "sha256": "7e451487994984b4d0e3fb3c8efbb87acef44b31cfcfefe5336c7b7f261dcc25" + } + ] + }, + { + "id": "node_rpm_source", + "label": "Node RPM 安装源与版本登记", + "frontend": { + "path": "manager/frontend/src/pages/repository/components/RepositoryNodeInstallManager.vue", + "methods": [ + "configureRepositoryNodeInstallSource", + "registerRepositoryNodeArtifact", + "listRepositoryNodeInstallSources", + "listRepositoryNodeArtifacts" + ] + }, + "backend": [ + "manager/backend/services/footstone/application/repository_node.go", + "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm_test.go", + "manager/frontend/src/pages/repository/components/RepositoryNodeInstallManager.test.ts" + ], + "evidence_level": "Actual Repository-backed publication and API registration; historical version selection observed in browser", + "implementation_boundary": "Historical selection fixture uses current code with an old version number; no running-node downgrade was executed. Unbound artifacts remain history only.", + "sources_match_frozen": true, + "raw_evidence": [ + { + "name": "node-rpm-artifacts-published.json", + "sha256": "987887bb3baee67dba942c8e54cae5f6d439fb6d700db9f5162df1995bb6d591" + }, + { + "name": "node-rpm-historical-v3.json", + "sha256": "f94635888dff71261c59bd97da7dfb788aeed9a9e34ea986ad5b6916659a4dc6" + }, + { + "name": "api-error-mapping-runtime.json", + "sha256": "cb74f6b0359ae16e6320b30c3d394b16bfbc76cac3e2a3e04e519c16e8591c58" + } + ] + }, + { + "id": "node_allowlist_install", + "label": "注册白名单与 TMS 安装仓库节点", + "frontend": { + "path": "manager/frontend/src/pages/repository/components/RepositoryNodeInstallManager.vue", + "methods": [ + "upsertRepositoryNodeRegistrationAllowlist", + "createRepositoryNodeEnrollment" + ] + }, + "backend": [ + "manager/backend/services/footstone/application/repository_node.go", + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle.go", + "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_mysql_authorization_test.go", + "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm_test.go" + ], + "evidence_level": "Native dual-architecture signed RPM, real TMS install and original Node identity readback; real MySQL authorization race", + "implementation_boundary": "TMS sends commands and immutable repo configuration, RPM body is downloaded from standard internal repository. No standalone file-distribution protocol.", + "sources_match_frozen": true, + "raw_evidence": [ + { + "name": "node-rpm-x86-install-final.json", + "sha256": "601a168d62296702ff872d85e189f32f8c735fcdd2bc228554eff3a625ec41d0" + }, + { + "name": "node-rpm-arm-install-recovered.json", + "sha256": "e58db39422567901f4e775f769df85a62b96e06754f150fc8d400c7ca1addf7b" + }, + { + "name": "mysql-race-result.json", + "sha256": "a6e5caec712d01f710b3f7f0daf614c528dffe52c9ec9e68802517a9d147d787" + } + ] + }, + { + "id": "certificate_recovery", + "label": "RPM 安装后的自动注册和证书精确恢复", + "frontend": { + "path": "manager/frontend/src/pages/repository/components/RepositoryNodeInstallManager.vue", + "methods": [ + "createRepositoryNodeEnrollment" + ] + }, + "backend": [ + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle.go", + "manager/backend/services/footstone/repositoryworker/bootstrap/client.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_mysql_authorization_test.go" + ], + "evidence_level": "Backend/Node bootstrap integration and actual auto-registration, not a separate browser certificate button", + "implementation_boundary": "Consumed token is not re-authorized; exact persisted authorization may resume issuance or return the same public certificate. Issuance remains outside authorization transaction.", + "sources_match_frozen": true, + "raw_evidence": [ + { + "name": "mysql-race-result.json", + "sha256": "a6e5caec712d01f710b3f7f0daf614c528dffe52c9ec9e68802517a9d147d787" + }, + { + "name": "node-rpm-arm-install-recovered.json", + "sha256": "e58db39422567901f4e775f769df85a62b96e06754f150fc8d400c7ca1addf7b" + } + ] + }, + { + "id": "package_install", + "label": "软件包多目标预览与安装", + "frontend": { + "path": "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.vue", + "methods": [ + "previewRepositoryPackageInstall", + "confirmRepositoryPackageInstall" + ] + }, + "backend": [ + "manager/backend/services/footstone/application/repository_package_install.go", + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.test.ts", + "manager/backend/services/footstone/domain/repositoryPackageInstallService/service_test.go" + ], + "evidence_level": "Actual per-Agent structured preview and one common business task with two successful targets", + "implementation_boundary": "Frontend does not loop single-node install API to manufacture unrelated tasks; immutable Revision stays bound.", + "sources_match_frozen": true, + "raw_evidence": [ + { + "name": "multitarget-final.json", + "sha256": "b37c4f78e05705882352ffb29a52a3a1d468eaa58fbf406353b7b53b8b75c8eb" + }, + { + "name": "global-task-map.json", + "sha256": "9b8256a8693ba3a1e48ae7a522927387070132e9ba968b86d65f1ef2e7243ad3" + } + ] + }, + { + "id": "indexed_upload", + "label": "多文件队列、暂停、恢复与刷新后重选", + "frontend": { + "path": "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "methods": [ + "beginRepositoryUpload", + "putRepositoryUploadChunk", + "describeRepositoryUploadChunks", + "pauseRepositoryUpload", + "resumeRepositoryUpload", + "completeRepositoryUpload", + "abortRepositoryUpload" + ] + }, + "backend": [ + "manager/backend/services/footstone/application/repository_content.go", + "manager/backend/services/footstone/domain/repositoryContentService/service.go", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/frontend/src/pages/repository/components/RepositoryUpload.queue.test.ts", + "manager/frontend/src/pages/repository/RepositoryUpload.completion.test.ts" + ], + "evidence_level": "Live browser forwarded requests use actual product responses; independent component tests clearly separated", + "implementation_boundary": "Concurrent file sessions are expected here. Evidence observed max two chunk requests, byte-weighted progress, server session restoration and missing-chunk resumption; browser File must be reselected after refresh.", + "sources_match_frozen": true, + "raw_evidence": [ + { + "name": "upload-queue-v2-live.json", + "sha256": "a0c742db30a73f407e376ac758309247709dc7542f433e0f25b8a6c8e1dfb5c7" + }, + { + "name": "upload-queue-v2-product-final.json", + "sha256": "d0370dcf0ea0eff6e30d5e6d96a50733ee2bd9aece208cbd1f730a1f5bc7913c" + } + ] + }, + { + "id": "dependency_batch_delete", + "label": "依赖预览及批量删除", + "frontend": { + "path": "manager/frontend/src/pages/repository/detail.vue", + "methods": [ + "previewRepositoryPackageDeletion", + "confirmRepositoryPackageDeletion" + ] + }, + "backend": [ + "manager/backend/services/footstone/application/repository_content.go", + "manager/backend/services/footstone/domain/repositoryContentService/package_delete.go", + "manager/backend/services/footstone/repositoryworker/ingestion.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryContentService/package_delete_test.go" + ], + "evidence_level": "Actual API Preview/Confirm and terminal task with two result items and one new revision", + "implementation_boundary": "One Confirm API creates one DELETE_PACKAGE task. No frontend loop over old single-package Delete API. Wrong digest and changed revision reject.", + "sources_match_frozen": true, + "raw_evidence": [ + { + "name": "product-api-evidence.json", + "sha256": "99dbae99d56be5b7e45d31963d7c13d92cfc5991b4f8918ac008a6f8ce785f27" + }, + { + "name": "terminal-smoke.json", + "sha256": "8624435c64f27754e4a5e3a56ec331063e8ef24a953d8b3072461ec3113be37e" + } + ] + }, + { + "id": "inventory_import_export", + "label": "Inventory 导出、映射预览与确认导入", + "frontend": { + "path": "manager/frontend/src/pages/repository/index.vue", + "methods": [ + "exportRepositoryInventory", + "previewRepositoryInventoryImport", + "confirmRepositoryInventoryImport" + ] + }, + "backend": [ + "manager/backend/services/footstone/application/repository_report.go", + "manager/backend/services/footstone/domain/repositoryInventoryService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryInventoryService/service_test.go" + ], + "evidence_level": "Actual export/import API and current common-task readback", + "implementation_boundary": "Import Job stores domain mapping/results and repository_task_id, not a second independent global task state machine.", + "sources_match_frozen": true, + "raw_evidence": [ + { + "name": "product-api-evidence.json", + "sha256": "99dbae99d56be5b7e45d31963d7c13d92cfc5991b4f8918ac008a6f8ce785f27" + }, + { + "name": "global-task-map.json", + "sha256": "9b8256a8693ba3a1e48ae7a522927387070132e9ba968b86d65f1ef2e7243ad3" + }, + { + "name": "post-rotation-product-checks.json", + "sha256": "455357fcb660113900194fe7833c30d1542d532a9e4a47b082c397e514869293" + } + ] + }, + { + "id": "schedule", + "label": "每日/每周计划与暂停恢复", + "frontend": { + "path": "manager/frontend/src/pages/repository/components/RepositorySyncSchedulePanel.vue", + "methods": [ + "describeRepositorySyncSchedule", + "upsertRepositorySyncSchedule", + "pauseRepositorySyncSchedule", + "resumeRepositorySyncSchedule" + ] + }, + "backend": [ + "manager/backend/services/footstone/application/repository_schedule.go", + "manager/backend/services/footstone/domain/repositoryScheduleService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryScheduleService/acceptance_integration_test.go" + ], + "evidence_level": "Actual daily/weekly UI configuration; MySQL-backed Scheduler Service; current SCHEDULED task readback", + "implementation_boundary": "Time progression and failure-threshold correctness were tested in the service with a controlled task enqueuer/reader, not by advancing production wall clock.", + "sources_match_frozen": true, + "raw_evidence": [ + { + "name": "schedule-result.json", + "sha256": "d8ed9e0028b997f21d710269c71fe55023a48b5b963eb4c1575b18c77555c294" + }, + { + "name": "schedule-api-config.json", + "sha256": "68266d9538bff0f60789b9ebd2f945ac6d77cf20bdd3685fb324e9f80f6f40f3" + }, + { + "name": "task-trigger-ui.json", + "sha256": "ce42676c6c47c2671b513609b40791c1b2cf01c14f158b18ef4a32873b8c3adc" + }, + { + "name": "global-task-map.json", + "sha256": "9b8256a8693ba3a1e48ae7a522927387070132e9ba968b86d65f1ef2e7243ad3" + } + ] + }, + { + "id": "distribution", + "label": "不可变 Revision 节点分发", + "frontend": { + "path": "manager/frontend/src/pages/repository/components/RepositoryDistribution.vue", + "methods": [ + "listRepositoryDistributionNodes", + "createRepositoryDistribution", + "describeRepositoryDistribution", + "exportRepositoryDistributionResult" + ] + }, + "backend": [ + "manager/backend/services/footstone/application/repository_distribution.go", + "manager/backend/services/footstone/domain/repositoryDistributionService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryDistributionService/service_test.go" + ], + "evidence_level": "Actual create API and successful two-Agent distribution evidence in public style summary", + "implementation_boundary": "Reuses TMS task and immutable Revision hash verification; target selector is not a fake static list.", + "sources_match_frozen": true, + "raw_evidence": [ + { + "name": "product-api-evidence.json", + "sha256": "99dbae99d56be5b7e45d31963d7c13d92cfc5991b4f8918ac008a6f8ce785f27" + }, + { + "name": "terminal-smoke.json", + "sha256": "8624435c64f27754e4a5e3a56ec331063e8ef24a953d8b3072461ec3113be37e" + } + ] + }, + { + "id": "gpg", + "label": "GPG 公钥登记与策略配置", + "frontend": { + "path": "manager/frontend/src/pages/repository/components/RepositoryGPGKeys.vue", + "methods": [ + "registerRepositoryGPGKey", + "listRepositoryGPGKeys", + "deleteRepositoryGPGKey" + ] + }, + "backend": [ + "manager/backend/services/footstone/application/repository_content.go", + "manager/backend/services/footstone/domain/repositoryContentService/service.go" + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryContentService/upload_session_test.go" + ], + "evidence_level": "Actual public-key registration, ENFORCE rejection/retry and signed publication; deletion wiring/service tests", + "implementation_boundary": "Private metadata-signing key stays on Node; Manager references key_ref and public metadata only.", + "sources_match_frozen": true, + "raw_evidence": [ + { + "name": "product-api-evidence.json", + "sha256": "99dbae99d56be5b7e45d31963d7c13d92cfc5991b4f8918ac008a6f8ce785f27" + }, + { + "name": "node-rpm-artifacts-published.json", + "sha256": "987887bb3baee67dba942c8e54cae5f6d439fb6d700db9f5162df1995bb6d591" + } + ] + } + ], + "public_evidence": [ + "docs/evidence/repository-style-validation-20261023.json", + "docs/evidence/repository-final-validation-20261024.json", + "docs/evidence/repository-taskflow-validation-20261024.json", + "docs/evidence/repository-node-rpm-error-validation-20261024.json", + "docs/evidence/repository-post-rotation-product-validation-20261024.json" + ], + "limitations": [ + "No current independent task sample for REBUILD_METADATA/PUBLISH/VALIDATE enum values; metadata/signature/atomic-publish stages are exercised inside real upload/sync tasks.", + "Historical RPM UI selection used current code under an older version label; no running-node downgrade claimed.", + "Creating/updating/deleting configuration is synchronous API work and does not necessarily create a Repository Task.", + "Successful API paths and runtime tests do not establish full browser-click coverage for every CRUD/allowlist edit; unclicked controls are labeled as wiring/service evidence.", + "No final status is promoted by this report." + ], + "observed_fake_mutating_controls": [], + "raw_audit_sha256": "d9e6b4b4a5a617cb34d60e5c97eca6ff38b484f1ea895bca1c9af9bb9e212209", + "business_code_changed": false, + "progress_status_changed": false, + "final_ready": false +} diff --git a/docs/evidence/repository-credential-rotation-20261024.json b/docs/evidence/repository-credential-rotation-20261024.json new file mode 100644 index 0000000000000000000000000000000000000000..69f250461353959db87fee5329ba241eecacfc80 --- /dev/null +++ b/docs/evidence/repository-credential-rotation-20261024.json @@ -0,0 +1,37 @@ +{ + "schema_version": 1, + "date": "2026-10-24", + "scope": "External OCAI provider key rotation and real Repository analysis", + "external_key_rotation": "PASS", + "old_key_revoked_confirmed_by_user": true, + "provider_authentication_http_status": 200, + "runtime_key_matches": true, + "service_healthy": true, + "image_unchanged": true, + "unrelated_services_unchanged": true, + "only_runtime_api_key_configuration_changed": true, + "actual_repository_analysis": { + "task_id": "bce358ed-50be-4146-96bd-2f49768ff0bc", + "error_code": "GPG_KEY_SYNC_REQUIRED", + "phase": "completed", + "read_only": true + }, + "secrets_in_report": false, + "key_values_in_repository": false, + "internal_authentication_secret_rotation": "PENDING", + "final_ready": false, + "evidence": [ + { + "name": "summary.json", + "sha256": "822a41d9522420c9922691b6793391319a741939942dfe98ccf0c2e6279a0a75" + }, + { + "name": "analysis-result.json", + "sha256": "b4fcb4ec80b76e8a3559640f199b98660949079daf880f9adcf55647cddba1a0" + }, + { + "name": "analysis-completed.png", + "sha256": "2b9075bf65e861318ba2effc6f374c53c8cab2a8dcfde0a7b74015ccc91eb7fe" + } + ] +} diff --git a/docs/evidence/repository-documentation-screenshots-20261024.json b/docs/evidence/repository-documentation-screenshots-20261024.json new file mode 100644 index 0000000000000000000000000000000000000000..a10dbd6e965e99834016c14b00d14c401b7d85ab --- /dev/null +++ b/docs/evidence/repository-documentation-screenshots-20261024.json @@ -0,0 +1,658 @@ +{ + "schema_version": 1, + "date": "2026-10-24", + "scope": "Current live UI screenshot refresh for four authoritative documents; documentary supplement to previously completed acceptance, not a new full business-test run", + "branch": "codex/repository-review-fixes-20261023", + "base_commit": "05312e2483b7827d07a89a6a29c3437c6a7dd42d", + "frontend_asset": "/assets/index-d5feec06.js", + "capture_method": "Playwright real browser; existing records and unconfirmed forms/previews; no DOM data replacement", + "screenshots": [ + { + "scene": "columns", + "path": "docs/images/repository/repository-current-20261024-columns.png", + "sha256": "fa98e662f7df2c50d0d0d172fb580f9c3753f7293a70463a8964168002c98b49", + "bytes": 18661, + "width": 520, + "height": 478, + "replaces": [ + "images/repository/repository-ui-20261022-columns.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "3bc2bd236bb1a08d670c9d3331ebd937cd9f69bccb41f7bcf6b31040d21fdbc3", + "address_redacted": true + }, + { + "scene": "distribution", + "path": "docs/images/repository/repository-current-20261024-distribution.png", + "sha256": "4379950b2ae8e2ff8c038b7d1682c641cfce1ddfbe81d0f37c9aac8634fe11a3", + "bytes": 59010, + "width": 1440, + "height": 1000, + "replaces": [ + "images/repository/repository-ui-20261022-distribution.png", + "images/repository/repository-style-20261023-distribution.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "5378fd9430a8f3d6839b9363e2f38275e468dc45a5e221637ff417b2bf887f3f", + "address_redacted": true + }, + { + "scene": "node-drawer", + "path": "docs/images/repository/repository-current-20261024-node-drawer.png", + "sha256": "7d442ea24f035d672c301d337331b39ff44109ea679f7cfb3bb78531eb44ce83", + "bytes": 68591, + "width": 1100, + "height": 1000, + "replaces": [ + "images/repository/repository-p13-node-view-sync.png", + "images/repository/repository-style-20261023-node-drawer.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "78c4991c95f28f1461368088f87f15f657ece3f7cbcade1b8ba5615aa8e9f141", + "address_redacted": true + }, + { + "scene": "package-install-preview", + "path": "docs/images/repository/repository-current-20261024-package-install-preview.png", + "sha256": "a35b3e9136e45b0bf6154621aaed3cb145d56739bad3dc603e9591dcec239cc1", + "bytes": 32211, + "width": 760, + "height": 522, + "replaces": [ + "images/repository/repository-p13-package-install-preview.png", + "images/repository/repository-ui-20261022-install-preview.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "6a4a6630394ee5cbb398be0e8360b9ebede06138519fdaa8bb0dcac51c172ab9", + "address_redacted": true + }, + { + "scene": "package-install-result", + "path": "docs/images/repository/repository-current-20261024-package-install-result.png", + "sha256": "af59f7fbf6161c5a916a9cdf38bd4afe308618d1850f6f6e797d41accf2ab0f2", + "bytes": 45748, + "width": 720, + "height": 1000, + "replaces": [ + "images/repository/repository-p13-package-install-targets.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "a6603f3cac5eb20b36efd124885a0af70d713b14871ba5a1326b8969d9a1ea32", + "address_redacted": true + }, + { + "scene": "batch-delete-preview", + "path": "docs/images/repository/repository-current-20261024-batch-delete-preview.png", + "sha256": "27cfe29d1d128e38a5862a1cd459c9933be678c6f8af23c5296fb77552dbeee3", + "bytes": 20498, + "width": 760, + "height": 337, + "replaces": [ + "images/repository/repository-p13-batch-delete-preview.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "69440748de5fe6f0d3c06a8dde9022d6db11e02562122cce686f64fa1baa162e", + "address_redacted": true + }, + { + "scene": "batch-delete-result", + "path": "docs/images/repository/repository-current-20261024-batch-delete-result.png", + "sha256": "072665209ec8c51e32532680f85da3a5b0147f2cc0ee67e97ce51665f0ae4ae1", + "bytes": 45758, + "width": 720, + "height": 1000, + "replaces": [ + "images/repository/repository-p13-batch-delete-result.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "047837e9b7e29030c8bf13926c405dbd2112b429de8ef1b2482b53aa49d9bace", + "address_redacted": true + }, + { + "scene": "inventory-import-preview", + "path": "docs/images/repository/repository-current-20261024-inventory-import-preview.png", + "sha256": "26617aaf4d968d826798e4d0513baf0d8454d80beb373fa5d4e7373910ff1d80", + "bytes": 35112, + "width": 880, + "height": 686, + "replaces": [ + "images/repository/repository-p13-inventory-import-preview.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "7b126fd463e799e5766a77abf344ee92fac895af7c361d5bf42e9f7416709873", + "address_redacted": true + }, + { + "scene": "inventory-import-task", + "path": "docs/images/repository/repository-current-20261024-inventory-import-task.png", + "sha256": "09d3fb03b673c49d594c991031b2d744fddb3c934ea8668eb4fe16ab7ad724f3", + "bytes": 28034, + "width": 720, + "height": 1000, + "replaces": [ + "images/repository/repository-p13-inventory-import-task.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "09d3fb03b673c49d594c991031b2d744fddb3c934ea8668eb4fe16ab7ad724f3", + "address_redacted": true + }, + { + "scene": "create-noarch-filter", + "path": "docs/images/repository/repository-current-20261024-create-noarch-filter.png", + "sha256": "0165ac2bc0ebaffc6c0e6d1d6aab3e82f327dbf763a1f617e2f3e4ee36951abe", + "bytes": 79683, + "width": 840, + "height": 1334, + "replaces": [ + "images/repository/repository-guide-04-noarch-filter.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "3bc938111d0966b7e08be444793fe72a550f854709d039b1f72e2365774f9b4b", + "address_redacted": true + }, + { + "scene": "sources", + "path": "docs/images/repository/repository-current-20261024-sources.png", + "sha256": "5f941c710384455832cb3fc652cfbbd41849ac3f0f717f6102ec516515d5d5ed", + "bytes": 90653, + "width": 1920, + "height": 1080, + "replaces": [ + "images/repository/repository-build141-upstream-sources.png", + "images/repository/repository-release-upstreams.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "5f941c710384455832cb3fc652cfbbd41849ac3f0f717f6102ec516515d5d5ed", + "address_redacted": true + }, + { + "scene": "create-upstream", + "path": "docs/images/repository/repository-current-20261024-create-upstream.png", + "sha256": "1b6824a0f58c9f5a0e0acc2b24f6e8aeb24d5ab9d8ca36b1a3b6748924030ad0", + "bytes": 50386, + "width": 840, + "height": 920, + "replaces": [ + "images/repository/repository-build141-create-upstream-options.png", + "images/repository/repository-release-create.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "8f9491830eeff380d67402147591a95b5dc33cf2ea9aa06da04063dd3aff5d97", + "address_redacted": true + }, + { + "scene": "create-filter", + "path": "docs/images/repository/repository-current-20261024-create-filter.png", + "sha256": "a3d369d745d37db4ee0879bc03e2f7c0a5bdda5f0af33991999ebbae9c3eea7d", + "bytes": 59936, + "width": 840, + "height": 920, + "replaces": [ + "images/repository/repository-build141-create-filter-options.png", + "images/repository/repository-release-create-filter.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "eafc88cfb93c9860ac045077cfc125a0df30bf5bcc55c99b45ea966450d3466d", + "address_redacted": true + }, + { + "scene": "list", + "path": "docs/images/repository/repository-current-20261024-list.png", + "sha256": "fdbce9214d5e6231affccb216d8747f6b5ffa0f8fb710e925914846a0b1ebf90", + "bytes": 132156, + "width": 1920, + "height": 1080, + "replaces": [ + "images/repository/repository-ui-20261022-list.png", + "images/repository/repository-style-20261023-list.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "86d30e195dc938e14c0d3e17048339efc719e1d2a8951c3a5add33ecbaba637f", + "address_redacted": true, + "header_search_masked": true + }, + { + "scene": "nodes", + "path": "docs/images/repository/repository-current-20261024-nodes.png", + "sha256": "12887dfe3b8c7c4591d339fa097ffc5f718122f7d5f7cf8f9c942cf5d0e0c556", + "bytes": 82742, + "width": 1440, + "height": 1000, + "replaces": [ + "images/repository/repository-release-nodes.png", + "images/repository/repository-style-20261023-nodes.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "48f8fcdf3ed471fa895dd37dc48954b0bc287bf8af3d5233cca3afbe6210b263", + "address_redacted": true, + "header_search_masked": true + }, + { + "scene": "node-install", + "path": "docs/images/repository/repository-current-20261024-node-install.png", + "sha256": "170f02c1032680f088a97c067030d472891672b44bebd2da15c6bb1bfce661e7", + "bytes": 50048, + "width": 1080, + "height": 758, + "replaces": [ + "images/repository/repository-release-node-install.png", + "images/repository/repository-final-20261024-node-version.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "350c2d54760e1aa09103026fe34da9afd2617a0f33aec0bc2fe0e550ee46c641", + "address_redacted": true + }, + { + "scene": "node-install-sources", + "path": "docs/images/repository/repository-current-20261024-node-install-sources.png", + "sha256": "5e654538831a51d0f53113a4bd77e2853ed93925a8e3d413e9d06e25871ee592", + "bytes": 98410, + "width": 1080, + "height": 1230, + "replaces": [ + "images/repository/repository-release-node-sources.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "74eb3808b1f2874d9695518db94b666821074e11e40692f768d06f51e047ec00", + "address_redacted": true + }, + { + "scene": "node-allowlist", + "path": "docs/images/repository/repository-current-20261024-node-allowlist.png", + "sha256": "2c8eacbbcffa17771796c5755295cff4a7aadfef8a4122e173656fe335bad4ed", + "bytes": 43564, + "width": 1080, + "height": 680, + "replaces": [ + "images/repository/repository-release-node-allowlist.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "ac5cdd551fb1cdac08c4eb8a9875acfd45a8f995a6288428556cef1bb694de82", + "address_redacted": true + }, + { + "scene": "overview", + "path": "docs/images/repository/repository-current-20261024-overview.png", + "sha256": "1a1ce162e42dd52c8f71ed7d500a80fe0e8c680af87e9bc7a3b66d26309f8986", + "bytes": 57522, + "width": 1440, + "height": 1000, + "replaces": [ + "images/repository/repository-release-overview.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "cab31530a5c42114527a5e46f191b1b8f8489d899fd69b460d2ada443374f819", + "address_redacted": true + }, + { + "scene": "packages", + "path": "docs/images/repository/repository-current-20261024-packages.png", + "sha256": "0d592f6564b0853d3cb92af4661444ce83213be3cb0a42f3f25ae453baa8a09e", + "bytes": 75325, + "width": 1440, + "height": 1000, + "replaces": [ + "images/repository/repository-release-packages.png", + "images/repository/repository-style-20261023-packages.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "a459ced84937da707f3dfe94da854366c4bb62ed3711270605121da317f91273", + "address_redacted": true + }, + { + "scene": "package-install-nodes", + "path": "docs/images/repository/repository-current-20261024-package-install-nodes.png", + "sha256": "493eafaae0f6b2418957030b71e9bb798e818b24dcda8b6afd1f718bd7bcb526", + "bytes": 37496, + "width": 760, + "height": 736, + "replaces": [ + "images/repository/repository-ui-20261022-install-nodes.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "c7ab1a71bcb1e0f012ca0ae5f626e4a6a9384043123821e134713202b50ecae5", + "address_redacted": true + }, + { + "scene": "upload-queue", + "path": "docs/images/repository/repository-current-20261024-upload-queue.png", + "sha256": "56ea1498804ca6b9ca13d766a7fbe023faa8c3097a94bcbff24a1438038007f0", + "bytes": 13221, + "width": 760, + "height": 1000, + "replaces": [ + "images/repository/repository-release-upload.png", + "images/repository/repository-final-20261024-upload-complete.png" + ], + "observation": "Current empty queue and file-selection entry; not a new successful upload", + "original_sha256": "63ca0b2975620f6d1ab5d54cb1cffdc14200b2cccbcb9a6bf6de362196246ffc", + "address_redacted": true + }, + { + "scene": "quarantine", + "path": "docs/images/repository/repository-current-20261024-quarantine.png", + "sha256": "b1fa6bcff2688a65a6f200acedd387a1ab370c0c05a5411fa3759acbcf84733a", + "bytes": 53257, + "width": 1440, + "height": 1000, + "replaces": [ + "images/repository/repository-release-quarantine.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "b1fa6bcff2688a65a6f200acedd387a1ab370c0c05a5411fa3759acbcf84733a", + "address_redacted": true + }, + { + "scene": "tasks", + "path": "docs/images/repository/repository-current-20261024-tasks.png", + "sha256": "f4be26776962a65e2b3494e0fa694c44315d3e200965a5b12c86c9875aed0bed", + "bytes": 89891, + "width": 1440, + "height": 1000, + "replaces": [ + "images/repository/repository-ui-20261022-tasks.png", + "images/repository/repository-style-20261023-tasks.png", + "images/repository/repository-final-20261024-tasks.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "da50936df032b8c8c29edf1b840f039ae0f1d91b560ee153ab586cd93308342b", + "address_redacted": true + }, + { + "scene": "task-failure", + "path": "docs/images/repository/repository-current-20261024-task-failure.png", + "sha256": "a014673bf0f9286f772b10fe098578c4acc6131ab20b1f7084e115afc4896198", + "bytes": 35004, + "width": 720, + "height": 1000, + "replaces": [ + "images/repository/repository-ui-20261022-task-failure.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "d354638f198eecd51c1eb556a560cbf3eae90d6968a2097a0bba57a291629f43", + "address_redacted": true + }, + { + "scene": "ocai-result", + "path": "docs/images/repository/repository-current-20261024-ocai-result.png", + "sha256": "24adfb941197373c9d6ad5613e1f7695fb1fa15be9705a6e7b306a9a77ed777c", + "bytes": 120414, + "width": 1440, + "height": 1000, + "replaces": [ + "images/repository/repository-ui-20261022-ocai-result.png" + ], + "observation": "Completed current task analysis viewed through full-page history; advice only", + "original_sha256": "79a536cf4ec9dd32e6b676621a07e277a0f048e0a7c99c26027bc27dbfa4bb70", + "address_redacted": true + }, + { + "scene": "config", + "path": "docs/images/repository/repository-current-20261024-config.png", + "sha256": "f88f032da5562d7873add469292283c1688e77227d557ea3fa726512dae9e051", + "bytes": 88039, + "width": 1440, + "height": 1000, + "replaces": [ + "images/repository/repository-release-config.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "5b07531db47c9af49d4f7a445b2bfca61cf3a2092741c6f8582fade8a862f363", + "address_redacted": true, + "header_search_masked": true + }, + { + "scene": "gpg", + "path": "docs/images/repository/repository-current-20261024-gpg.png", + "sha256": "c04ad6639a0308dbce7eccc52d7b4411ef41226abe8e7df748f5d869d5c6ee46", + "bytes": 70643, + "width": 1440, + "height": 1000, + "replaces": [ + "images/repository/repository-release-gpg.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "ab6064286881406fd5c7cd64a04fce30ebc9076a59dffb813c67825eebc2e1c9", + "address_redacted": true + }, + { + "scene": "distribution-detail", + "path": "docs/images/repository/repository-current-20261024-distribution-detail.png", + "sha256": "56a417d0666024c2b14ce25874991ed0cb363bd5f0e617f5219e3dd6759104b5", + "bytes": 109024, + "width": 720, + "height": 1000, + "replaces": [ + "images/repository/repository-ui-20261022-distribution-detail.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "beb6938d8942834c29b9e718933fc1a582181e5b1f975377ba6134f737ead9a1", + "address_redacted": true + }, + { + "scene": "source-form", + "path": "docs/images/repository/repository-current-20261024-source-form.png", + "sha256": "f9d9244346d8cf30669fb81e93644fd151db6048d37510cfff1da4b0efcb5e41", + "bytes": 33574, + "width": 760, + "height": 807, + "replaces": [ + "images/repository/repository-release-upstream-form.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "f9d9244346d8cf30669fb81e93644fd151db6048d37510cfff1da4b0efcb5e41", + "address_redacted": true + }, + { + "scene": "ocai-context", + "path": "docs/images/repository/repository-current-20261024-ocai-context.png", + "sha256": "97f48640be09a41187ee43c81b1958c8571eb6f93284e437572efbbe1ffb25f3", + "bytes": 84688, + "width": 720, + "height": 753, + "replaces": [ + "images/repository/repository-ui-20261022-ocai-context.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "3da65fe4f3c2c4a9c446729a7081ceea8486855eeb43ee901b5eb53b274bacfb", + "address_redacted": true + }, + { + "scene": "batch20-packages", + "path": "docs/images/repository/repository-current-20261024-batch20-packages.png", + "sha256": "1f669953588caf59af4853ea1b019b6d9c3b31abbd67fffaf991d00738c18f22", + "bytes": 96934, + "width": 1440, + "height": 1000, + "replaces": [ + "images/repository/repository-batch20-packages.png" + ], + "observation": "Existing acc-20261022-sync repository with twenty packages viewed in current UI", + "original_sha256": "c6f5d624ed4bea0eb194ec33bf5faf9f5446b3a0fae8a689a73b413e9add8f8e", + "address_redacted": true + }, + { + "scene": "batch20-distribution-detail", + "path": "docs/images/repository/repository-current-20261024-batch20-distribution-detail.png", + "sha256": "56a417d0666024c2b14ce25874991ed0cb363bd5f0e617f5219e3dd6759104b5", + "bytes": 109024, + "width": 720, + "height": 1000, + "replaces": [ + "images/repository/repository-batch20-distribution-detail.png" + ], + "observation": "General existing two-package distribution example; not historical twenty-package evidence", + "original_sha256": "beb6938d8942834c29b9e718933fc1a582181e5b1f975377ba6134f737ead9a1", + "address_redacted": true + }, + { + "scene": "arm-node-drawer", + "path": "docs/images/repository/repository-current-20261024-arm-node-drawer.png", + "sha256": "06ce9022aca0c477d6a93c7e8acc44dfe2a90a64a96d7112de9d898426d97978", + "bytes": 60434, + "width": 1100, + "height": 1000, + "replaces": [ + "images/repository/repository-p13-arm-node-repository.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "ce977d7dd9d45eaa899ab83642ced0316b1e0767e711552535ac57be6513e7d5", + "address_redacted": true + }, + { + "scene": "command-helper-rerun", + "path": "docs/images/repository/repository-current-20261024-command-helper-rerun.png", + "sha256": "ff2b62e5b6b319bdc573c11374c321d9e6a1851980e3afab9fe344cc24a9ca81", + "bytes": 28254, + "width": 576, + "height": 1000, + "replaces": [ + "images/repository/repository-review-20261023-rerun.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "ff2b62e5b6b319bdc573c11374c321d9e6a1851980e3afab9fe344cc24a9ca81", + "address_redacted": true + }, + { + "scene": "command-helper-preview", + "path": "docs/images/repository/repository-current-20261024-command-helper-preview.png", + "sha256": "f47c511db5800ef17942f1eecac7063635c2b5f63ea5258ccc9e5b113b43c60c", + "bytes": 14464, + "width": 1000, + "height": 394, + "replaces": [ + "images/repository/repository-review-20261023-command-preview.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "f47c511db5800ef17942f1eecac7063635c2b5f63ea5258ccc9e5b113b43c60c", + "address_redacted": true + }, + { + "scene": "disk-chart", + "path": "docs/images/repository/repository-current-20261024-disk-chart.png", + "sha256": "389d197a1be6ac89f080a0cf88919950d74dda652f7231974edef1f5750b715b", + "bytes": 107673, + "width": 1440, + "height": 1000, + "replaces": [ + "images/repository/repository-review-20261023-optical.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "ab0748704948cdeb52f866117ecb14a6ed5cdebc7e5a01a57b65fa1b4556d6c2", + "address_redacted": true + }, + { + "scene": "task-success", + "path": "docs/images/repository/repository-current-20261024-task-success.png", + "sha256": "af59f7fbf6161c5a916a9cdf38bd4afe308618d1850f6f6e797d41accf2ab0f2", + "bytes": 45748, + "width": 720, + "height": 1000, + "replaces": [ + "images/repository/repository-style-20261023-task-drawer.png" + ], + "observation": "Current live product UI; existing records or unconfirmed forms/previews", + "original_sha256": "a6603f3cac5eb20b36efd124885a0af70d713b14871ba5a1326b8969d9a1ea32", + "address_redacted": true + } + ], + "documents": [ + { + "path": "docs/私有化软件仓库方案设计.md", + "before_sha256": "4729b5900e352d954b73dca0fcbf541d1ecc21ddfb11e9b4bd3eee9cea66236a", + "sha256": "4b30a0e719cfcb4fdbbcd544abb47afefe5b58732f160b997c01e749cca4e1d9", + "image_references": 21, + "current_ui_references": 16 + }, + { + "path": "docs/私有化软件仓库部署说明.md", + "before_sha256": "75551ad9e544d34167541a0073bec0a4c9565c5384ab4910f26212bc61d3f172", + "sha256": "5cf4f8396ce47c6dbf42b6e488d8d1e1ed47d3efb1526c56c3fee936f785d27a", + "image_references": 4, + "current_ui_references": 4 + }, + { + "path": "docs/私有化软件仓库测试验收.md", + "before_sha256": "bc1f6ef2c656ae553b6ba66073578921eeb742e332e8eeed9b420e68e3fdf4ad", + "sha256": "99100abe4054736b238f20451c925d2e88a3f8594fb00a9b7eb29c55498b67c4", + "image_references": 8, + "current_ui_references": 8 + }, + { + "path": "docs/私有化软件仓库结构化验收报告.md", + "before_sha256": "ecb711b3e0ba4ac9279d7bc3b087b457f8e0a9a98d4999a49cc3a805ab25e43b", + "sha256": "e109e0ee4c6692ff2e8e219448fa583cba7f9a522c07dde925791208cd51ce1a", + "image_references": 71, + "current_ui_references": 66 + } + ], + "checks": { + "status": "PASS", + "screenshots": 38, + "unique_image_content": 36, + "current_ui_references": 94, + "old_embedded_ui_references": 0, + "local_references_checked": 222, + "missing_references": 0, + "historical_images_preserved": 56, + "architecture_diagrams_preserved": 5, + "pdf_unchanged": true, + "core_files_checked": 1467, + "core_unchanged": true, + "acceptance_ids_checked": 152, + "acceptance_references_checked": 898 + }, + "boundaries": [ + "Empty upload queue is an entry example; previous upload evidence is retained", + "Installation, deletion and import previews were not confirmed", + "Historical twenty-package results are not replaced by a two-package distribution example", + "OCAI suggestions were viewed, not executed", + "Original PNG and PDF evidence preserved", + "No commit, push or PR update" + ], + "documentation_only": true, + "visual_review": { + "status": "PASS", + "recaptured_after_review": [ + "config", + "quarantine", + "inventory-import-preview", + "create-noarch-filter", + "ocai-result", + "source-form", + "node-install-sources", + "node-install", + "inventory-import-task", + "task-success", + "package-install-result", + "batch-delete-result" + ], + "notes": [ + "Noarch uses current platform-independent group; former x86 selections discarded", + "Fixed immutable revision configuration fully loaded", + "Quarantine view captured after Drawer closes; current repository has zero quarantine entries", + "Inventory preview uses one existing definition selected from product export and SKIP conflict policy; not confirmed", + "OCAI reasoning collapsed and completed answer visible; no suggestion executed", + "Node installation source and form captured with stable layout and no installation", + "Task Drawers recaptured with transitions disabled and content wrapper only, preventing off-screen clipping", + "Published screenshots hide test addresses; originals remain local and are excluded from outgoing Git objects" + ] + }, + "release_check": { + "command": "bash scripts/repository-release-check.sh", + "exit_code": 0, + "candidate_files_scanned": 1734, + "log_sha256": "305846469d6f23a437090b7897d6adede26452e3fd4a52e606370a7c122b2f71", + "scope": "Shell syntax, JSON, documentation assets, secret patterns and portability; not a new full business-test run" + }, + "privacy_publication": { + "date": "2026-10-25", + "status": "PASS", + "address_redacted": true, + "original_images_retained_locally": true, + "local_pdf_excluded": true, + "method": "Opaque screenshot privacy masks; real records and results are unchanged", + "publication_scope": "Documentation/screenshot privacy supplement to the 2026-10-24 tested candidate; not a new full business test", + "header_search_masked": true + } +} diff --git a/docs/evidence/repository-documentation-validation-20261024.json b/docs/evidence/repository-documentation-validation-20261024.json new file mode 100644 index 0000000000000000000000000000000000000000..da8044d44ff4557e20b6a1ba780ed73370e5e3ff --- /dev/null +++ b/docs/evidence/repository-documentation-validation-20261024.json @@ -0,0 +1,1591 @@ +{ + "schema_version": 1, + "date": "2026-10-24", + "scope": "Documentation content coverage, local references, and current candidate portability; not complete functional acceptance", + "documentation_coverage": "PASS", + "expected_ids": 152, + "mapped_ids": 152, + "topics": [ + { + "category": "UI", + "topic": "一级导航、列表、查询区、Drawer、状态标签和最新截图", + "sections": { + "design": { + "path": "docs/私有化软件仓库方案设计.md", + "heading": "8. 前端页面设计", + "line": 820 + }, + "deployment": { + "path": "docs/私有化软件仓库部署说明.md", + "heading": "4.1 只升级本次前端交互增量", + "line": 221 + }, + "testing": { + "path": "docs/私有化软件仓库测试验收.md", + "heading": "13. 界面统一与已部署候选的联验", + "line": 855 + }, + "acceptance_report": { + "path": "docs/私有化软件仓库结构化验收报告.md", + "heading": "5. 前端设计", + "line": 626 + } + } + }, + { + "category": "TASK", + "topic": "全局任务、目标结果、统一日志及进度", + "sections": { + "design": { + "path": "docs/私有化软件仓库方案设计.md", + "heading": "18.2 任务和流程模型", + "line": 1240 + }, + "deployment": { + "path": "docs/私有化软件仓库部署说明.md", + "heading": "6.2 节点运维与任务配置", + "line": 474 + }, + "testing": { + "path": "docs/私有化软件仓库测试验收.md", + "heading": "7.2 按功能复现的前置条件与通过标准", + "line": 706 + }, + "acceptance_report": { + "path": "docs/私有化软件仓库结构化验收报告.md", + "heading": "5.9 任务总览、日志与 OCAI", + "line": 741 + } + } + }, + { + "category": "NODE", + "topic": "节点运维、容量、Node RPM安装源、EVR、白名单和证书恢复", + "sections": { + "design": { + "path": "docs/私有化软件仓库方案设计.md", + "heading": "18.3 Node RPM 产品闭环", + "line": 1256 + }, + "deployment": { + "path": "docs/私有化软件仓库部署说明.md", + "heading": "5. 准备首个节点与从前端安装后续节点", + "line": 265 + }, + "testing": { + "path": "docs/私有化软件仓库测试验收.md", + "heading": "15. 当前候选补验记录", + "line": 907 + }, + "acceptance_report": { + "path": "docs/私有化软件仓库结构化验收报告.md", + "heading": "7.1 准备仓库节点与安装源", + "line": 893 + } + } + }, + { + "category": "OCAI", + "topic": "真实失败任务上下文、分析、只读边界与认证后复查", + "sections": { + "design": { + "path": "docs/私有化软件仓库方案设计.md", + "heading": "6.5 OCAI 智能辅助", + "line": 763 + }, + "deployment": { + "path": "docs/私有化软件仓库部署说明.md", + "heading": "8. OCAI 部署边界", + "line": 599 + }, + "testing": { + "path": "docs/私有化软件仓库测试验收.md", + "heading": "15.2 认证轮换后的产品复查", + "line": 1023 + }, + "acceptance_report": { + "path": "docs/私有化软件仓库结构化验收报告.md", + "heading": "11.9 运行认证和错误目录后续复查", + "line": 1780 + } + } + }, + { + "category": "ERROR", + "topic": "唯一错误目录、英文Code、中文原因和建议、技术原因", + "sections": { + "design": { + "path": "docs/私有化软件仓库方案设计.md", + "heading": "20.3 系统安装源错误响应与运行认证", + "line": 1308 + }, + "deployment": { + "path": "docs/私有化软件仓库部署说明.md", + "heading": "11.8 认证轮换与验收数据库隔离", + "line": 854 + }, + "testing": { + "path": "docs/私有化软件仓库测试验收.md", + "heading": "15.3 已部署错误目录检查", + "line": 1029 + }, + "acceptance_report": { + "path": "docs/私有化软件仓库结构化验收报告.md", + "heading": "4.11 错误目录与排障信息", + "line": 620 + } + } + }, + { + "category": "TMS", + "topic": "多Target包安装、不可变Revision、Preview/Confirm、TMS接入", + "sections": { + "design": { + "path": "docs/私有化软件仓库方案设计.md", + "heading": "18.2 任务和流程模型", + "line": 1240 + }, + "deployment": { + "path": "docs/私有化软件仓库部署说明.md", + "heading": "5.1 仓库节点与分发目标 Agent", + "line": 309 + }, + "testing": { + "path": "docs/私有化软件仓库测试验收.md", + "heading": "7.2 按功能复现的前置条件与通过标准", + "line": 706 + }, + "acceptance_report": { + "path": "docs/私有化软件仓库结构化验收报告.md", + "heading": "7.7 多节点安装与批量删除", + "line": 1009 + } + } + }, + { + "category": "SYNC", + "topic": "远程同步、Schedule、幂等、限速并发及Mirror/Append", + "sections": { + "design": { + "path": "docs/私有化软件仓库方案设计.md", + "heading": "5.2 远程同步落盘", + "line": 603 + }, + "deployment": { + "path": "docs/私有化软件仓库部署说明.md", + "heading": "6.2 节点运维与任务配置", + "line": 474 + }, + "testing": { + "path": "docs/私有化软件仓库测试验收.md", + "heading": "15. 当前候选补验记录", + "line": 907 + }, + "acceptance_report": { + "path": "docs/私有化软件仓库结构化验收报告.md", + "heading": "7.3 检查概览、执行同步与计划同步", + "line": 935 + } + } + }, + { + "category": "UPLOAD", + "topic": "Indexed Chunk、多文件、字节加权进度、暂停刷新重选与隔离", + "sections": { + "design": { + "path": "docs/私有化软件仓库方案设计.md", + "heading": "20.2 授权恢复与多文件队列补强", + "line": 1300 + }, + "deployment": { + "path": "docs/私有化软件仓库部署说明.md", + "heading": "11.7 当前候选的补验前提", + "line": 842 + }, + "testing": { + "path": "docs/私有化软件仓库测试验收.md", + "heading": "15. 当前候选补验记录", + "line": 907 + }, + "acceptance_report": { + "path": "docs/私有化软件仓库结构化验收报告.md", + "heading": "7.4 上传真实 RPM 或多个文件", + "line": 947 + } + } + }, + { + "category": "DELETE", + "topic": "依赖Preview、单任务批量删除、一次重建repodata", + "sections": { + "design": { + "path": "docs/私有化软件仓库方案设计.md", + "heading": "18.2 任务和流程模型", + "line": 1240 + }, + "deployment": { + "path": "docs/私有化软件仓库部署说明.md", + "heading": "10.1 含 GPG 公钥仓库的删除顺序", + "line": 723 + }, + "testing": { + "path": "docs/私有化软件仓库测试验收.md", + "heading": "7.2 按功能复现的前置条件与通过标准", + "line": 706 + }, + "acceptance_report": { + "path": "docs/私有化软件仓库结构化验收报告.md", + "heading": "7.7 多节点安装与批量删除", + "line": 1009 + } + } + }, + { + "category": "GPG", + "topic": "包签名、元数据签名、信任准备、密钥与真实DNF安装", + "sections": { + "design": { + "path": "docs/私有化软件仓库方案设计.md", + "heading": "7.1 GPG", + "line": 773 + }, + "deployment": { + "path": "docs/私有化软件仓库部署说明.md", + "heading": "7.2 Repository Metadata 签名密钥", + "line": 547 + }, + "testing": { + "path": "docs/私有化软件仓库测试验收.md", + "heading": "2.2 GPG 的两类复现入口", + "line": 236 + }, + "acceptance_report": { + "path": "docs/私有化软件仓库结构化验收报告.md", + "heading": "7.6 包级 GPG、元数据签名与 Repo 配置", + "line": 982 + } + } + }, + { + "category": "IMPORT", + "topic": "Inventory定义导入导出、预览、映射、统一Task和无Secret", + "sections": { + "design": { + "path": "docs/私有化软件仓库方案设计.md", + "heading": "18.4 Inventory 与机器验收", + "line": 1264 + }, + "deployment": { + "path": "docs/私有化软件仓库部署说明.md", + "heading": "6.2 节点运维与任务配置", + "line": 474 + }, + "testing": { + "path": "docs/私有化软件仓库测试验收.md", + "heading": "7.2 按功能复现的前置条件与通过标准", + "line": 706 + }, + "acceptance_report": { + "path": "docs/私有化软件仓库结构化验收报告.md", + "heading": "7.9 清单导入、任务检索与导出", + "line": 1047 + } + } + }, + { + "category": "INTEGRATION", + "topic": "真实后端连接、原链路回归、迁移、产品恢复和自动化", + "sections": { + "design": { + "path": "docs/私有化软件仓库方案设计.md", + "heading": "15. 最终发布门禁", + "line": 1164 + }, + "deployment": { + "path": "docs/私有化软件仓库部署说明.md", + "heading": "11.4 部署后门禁", + "line": 801 + }, + "testing": { + "path": "docs/私有化软件仓库测试验收.md", + "heading": "15. 当前候选补验记录", + "line": 907 + }, + "acceptance_report": { + "path": "docs/私有化软件仓库结构化验收报告.md", + "heading": "11. 当前版本结构化验收闭环", + "line": 1627 + } + } + }, + { + "category": "QUALITY", + "topic": "代码质量、构建、真实配置测试、数据库隔离", + "sections": { + "design": { + "path": "docs/私有化软件仓库方案设计.md", + "heading": "10. 验证与验收设计", + "line": 977 + }, + "deployment": { + "path": "docs/私有化软件仓库部署说明.md", + "heading": "11. 发布与运行检查", + "line": 779 + }, + "testing": { + "path": "docs/私有化软件仓库测试验收.md", + "heading": "15.1 独立容器 MySQL 模式", + "line": 955 + }, + "acceptance_report": { + "path": "docs/私有化软件仓库结构化验收报告.md", + "heading": "11.8 当前候选补验", + "line": 1754 + } + } + }, + { + "category": "E2E", + "topic": "双架构、单多节点、单多Agent、破坏恢复及安全负向", + "sections": { + "design": { + "path": "docs/私有化软件仓库方案设计.md", + "heading": "10. 验证与验收设计", + "line": 977 + }, + "deployment": { + "path": "docs/私有化软件仓库部署说明.md", + "heading": "11.3 双架构与可选拓扑", + "line": 795 + }, + "testing": { + "path": "docs/私有化软件仓库测试验收.md", + "heading": "7.2 按功能复现的前置条件与通过标准", + "line": 706 + }, + "acceptance_report": { + "path": "docs/私有化软件仓库结构化验收报告.md", + "heading": "11.8 当前候选补验", + "line": 1754 + } + } + }, + { + "category": "GOVERNANCE", + "topic": "固定152集合、四文档、证据、候选源码冻结和最终判断", + "sections": { + "design": { + "path": "docs/私有化软件仓库方案设计.md", + "heading": "15. 最终发布门禁", + "line": 1164 + }, + "deployment": { + "path": "docs/私有化软件仓库部署说明.md", + "heading": "11.4 部署后门禁", + "line": 801 + }, + "testing": { + "path": "docs/私有化软件仓库测试验收.md", + "heading": "7.3 机器验收命令", + "line": 730 + }, + "acceptance_report": { + "path": "docs/私有化软件仓库结构化验收报告.md", + "heading": "11.2 最终判定规则", + "line": 1651 + } + } + } + ], + "documents": [ + { + "path": "docs/私有化软件仓库方案设计.md", + "sha256": "4b30a0e719cfcb4fdbbcd544abb47afefe5b58732f160b997c01e749cca4e1d9" + }, + { + "path": "docs/私有化软件仓库部署说明.md", + "sha256": "5cf4f8396ce47c6dbf42b6e488d8d1e1ed47d3efb1526c56c3fee936f785d27a" + }, + { + "path": "docs/私有化软件仓库测试验收.md", + "sha256": "99100abe4054736b238f20451c925d2e88a3f8594fb00a9b7eb29c55498b67c4" + }, + { + "path": "docs/私有化软件仓库结构化验收报告.md", + "sha256": "e109e0ee4c6692ff2e8e219448fa583cba7f9a522c07dde925791208cd51ce1a" + } + ], + "links_check": { + "checked": 222, + "missing": [], + "links": [ + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "私有化软件仓库部署说明.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "私有化软件仓库测试验收.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "私有化软件仓库结构化验收报告.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "images/repository/repo-architecture.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "images/repository/repo-architecture.svg", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "images/repository/repo-feature-map.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "images/repository/repo-feature-map.svg", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "images/repository/repo-er-model.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "images/repository/repo-er-model.svg", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "images/repository/repo-publish-flow.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "images/repository/repo-publish-flow.svg", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "images/repository/repo-task-state.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "images/repository/repo-task-state.svg", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "私有化软件仓库结构化验收报告.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "evidence/repository-documentation-screenshots-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "images/repository/repository-current-20261024-columns.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "images/repository/repository-current-20261024-distribution.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "images/repository/repository-current-20261024-node-drawer.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "images/repository/repository-current-20261024-package-install-preview.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "images/repository/repository-current-20261024-package-install-result.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "images/repository/repository-current-20261024-batch-delete-preview.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "images/repository/repository-current-20261024-batch-delete-result.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "images/repository/repository-current-20261024-inventory-import-preview.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "images/repository/repository-current-20261024-inventory-import-task.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "私有化软件仓库结构化验收报告.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "architecture/private-repository-plan.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "私有化软件仓库部署说明.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "私有化软件仓库测试验收.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "私有化软件仓库结构化验收报告.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "OCAI-DEPLOY.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "../manager/backend/protocol/repository/repository.proto", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "../manager/frontend/src/service/repository.ts", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "images/repository/README.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "images/repository/repository-current-20261024-node-drawer.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "images/repository/repository-current-20261024-package-install-preview.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "images/repository/repository-current-20261024-package-install-result.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "images/repository/repository-current-20261024-batch-delete-preview.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "images/repository/repository-current-20261024-batch-delete-result.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "images/repository/repository-current-20261024-inventory-import-preview.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "images/repository/repository-current-20261024-inventory-import-task.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "私有化软件仓库结构化验收报告.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "私有化软件仓库结构化验收报告.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "私有化软件仓库结构化验收报告.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "私有化软件仓库测试验收.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "evidence/repository-node-rpm-error-validation-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "evidence/repository-credential-rotation-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "evidence/repository-internal-authentication-rotation-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "evidence/repository-final-validation-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "evidence/repository-acceptance-progress-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "evidence/repository-release-validation-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库方案设计.md", + "target": "evidence/repository-action-traceability-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库部署说明.md", + "target": "私有化软件仓库方案设计.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库部署说明.md", + "target": "私有化软件仓库测试验收.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库部署说明.md", + "target": "私有化软件仓库结构化验收报告.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库部署说明.md", + "target": "私有化软件仓库结构化验收报告.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库部署说明.md", + "target": "私有化软件仓库测试验收.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库部署说明.md", + "target": "evidence/repository-documentation-screenshots-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库部署说明.md", + "target": "images/repository/repository-current-20261024-nodes.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库部署说明.md", + "target": "images/repository/repository-current-20261024-node-install-sources.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库部署说明.md", + "target": "images/repository/repository-current-20261024-node-install.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库部署说明.md", + "target": "images/repository/repository-current-20261024-config.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库部署说明.md", + "target": "OCAI-DEPLOY.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库部署说明.md", + "target": "私有化软件仓库测试验收.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库部署说明.md", + "target": "私有化软件仓库结构化验收报告.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库部署说明.md", + "target": "私有化软件仓库测试验收.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库部署说明.md", + "target": "私有化软件仓库测试验收.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库部署说明.md", + "target": "evidence/repository-style-validation-20261023.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库部署说明.md", + "target": "evidence/repository-internal-authentication-rotation-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库部署说明.md", + "target": "私有化软件仓库测试验收.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库部署说明.md", + "target": "私有化软件仓库测试验收.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库部署说明.md", + "target": "evidence/repository-node-rpm-error-validation-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库部署说明.md", + "target": "evidence/repository-final-validation-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库部署说明.md", + "target": "evidence/repository-acceptance-progress-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库部署说明.md", + "target": "evidence/repository-release-validation-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "私有化软件仓库方案设计.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "私有化软件仓库部署说明.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "私有化软件仓库结构化验收报告.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "evidence/repository-documentation-screenshots-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "evidence/repository-ui-captures-20261022.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "私有化软件仓库结构化验收报告.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "私有化软件仓库结构化验收报告.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "images/repository/repository-current-20261024-create-noarch-filter.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "images/repository/repository-current-20261024-sources.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "images/repository/repository-current-20261024-create-upstream.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "images/repository/repository-current-20261024-create-filter.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "images/repository/repository-current-20261024-node-drawer.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "images/repository/repository-current-20261024-package-install-result.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "images/repository/repository-current-20261024-batch-delete-result.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "images/repository/repository-current-20261024-inventory-import-task.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "私有化软件仓库方案设计.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "私有化软件仓库结构化验收报告.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "evidence/repository-acceptance-20261014.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "evidence/repository-review-fixes-20261023.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "evidence/repository-style-validation-20261023.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "私有化软件仓库结构化验收报告.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "evidence/repository-recovery-validation-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "evidence/repository-final-validation-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "evidence/repository-acceptance-progress-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "evidence/repository-credential-rotation-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "evidence/repository-internal-authentication-rotation-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "evidence/repository-internal-authentication-rotation-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "evidence/repository-post-rotation-product-validation-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "evidence/repository-node-rpm-error-validation-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "evidence/repository-final-validation-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "evidence/repository-acceptance-progress-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库测试验收.md", + "target": "evidence/repository-release-validation-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "私有化软件仓库方案设计.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "私有化软件仓库部署说明.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "私有化软件仓库测试验收.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repo-architecture.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repo-architecture.svg", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repo-feature-map.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repo-feature-map.svg", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repo-publish-flow.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repo-publish-flow.svg", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repo-task-state.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repo-task-state.svg", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repo-er-model.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repo-er-model.svg", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/README.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "evidence/repository-documentation-screenshots-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "evidence/repository-ui-captures-20261022.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "evidence/repository-ui-captures-20261015.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-list.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-columns.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-create-upstream.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-create-filter.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-sources.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-nodes.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-node-drawer.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-node-install.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-node-install-sources.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-node-allowlist.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-overview.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-packages.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-package-install-nodes.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-package-install-preview.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-batch-delete-preview.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-upload-queue.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-quarantine.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-tasks.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-task-failure.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-ocai-result.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-config.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-gpg.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-distribution.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-distribution-detail.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-inventory-import-preview.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "私有化软件仓库测试验收.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "私有化软件仓库部署说明.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "私有化软件仓库部署说明.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-nodes.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-node-drawer.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-node-install.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-node-install-sources.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-node-allowlist.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-create-upstream.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-create-filter.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-source-form.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-overview.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-node-drawer.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-upload-queue.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-packages.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-quarantine.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-task-failure.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-ocai-context.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-ocai-result.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-gpg.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-config.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "私有化软件仓库测试验收.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-package-install-nodes.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-package-install-preview.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-package-install-preview.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-package-install-result.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-batch-delete-preview.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-batch-delete-result.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-distribution.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-distribution-detail.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-inventory-import-preview.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-tasks.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "私有化软件仓库测试验收.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-batch20-packages.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-batch20-distribution-detail.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "私有化软件仓库方案设计.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "私有化软件仓库部署说明.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "私有化软件仓库测试验收.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "architecture/private-repository-plan.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "../manager/backend/protocol/repository/repository.proto", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "../manager/backend/protocol/repositorynode/repository_node.proto", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "../manager/frontend/src/service/repository.ts", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "../manager/deployments/compose/repository-node.compose.yml", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "../scripts/repository-acceptance-matrix.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "evidence/repository-acceptance-20261014.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-node-drawer.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-batch-delete-result.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-arm-node-drawer.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-command-helper-rerun.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-command-helper-preview.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-disk-chart.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "evidence/repository-review-fixes-20261023.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "私有化软件仓库测试验收.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-list.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-task-success.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "evidence/repository-style-validation-20261023.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "私有化软件仓库测试验收.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "evidence/repository-recovery-validation-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "私有化软件仓库测试验收.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "私有化软件仓库测试验收.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "evidence/repository-final-validation-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-upload-queue.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-node-install.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "images/repository/repository-current-20261024-tasks.png", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "evidence/repository-acceptance-progress-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "evidence/repository-credential-rotation-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "evidence/repository-internal-authentication-rotation-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "evidence/repository-internal-authentication-rotation-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "evidence/repository-post-rotation-product-validation-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "evidence/repository-node-rpm-error-validation-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "私有化软件仓库测试验收.md", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "evidence/repository-final-validation-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "evidence/repository-acceptance-progress-20261024.json", + "exists": true + }, + { + "file": "docs/私有化软件仓库结构化验收报告.md", + "target": "evidence/repository-release-validation-20261024.json", + "exists": true + } + ] + }, + "source_pointer_repair": { + "repaired": 15, + "remaining_missing": 0, + "statuses_unchanged": true + }, + "release_check": { + "command": "bash scripts/repository-release-check.sh", + "execution_environment": "WSL Linux operating on current Git candidate; no product installation performed", + "exit_code": 0, + "scanned_files": 1690, + "scope": "Shell syntax, JSON, documentation assets, secret pattern scan, portability and Git whitespace" + }, + "evidence": [ + { + "name": "topic-document-mapping.json", + "sha256": "a9a493a3ce13ace0c42230eec078bb1744f7cdbdf57510d0673aee8252134a36" + }, + { + "name": "markdown-links-audit.json", + "sha256": "a2ab9af0a88373d93af363dba588f2f548b81b882894f1fab6229f2286d21446" + }, + { + "name": "document-after-manifest.json", + "sha256": "8130290743c7758f6fb27f13122547d146009392b61b4410bf46f07c31e687f6" + } + ], + "final_ready": true, + "updated_at": "2026-10-24T11:44:39.793Z", + "p14_core_unchanged": true, + "p14_gate_sha256": "434935a61f24c2b66ab22d148aca025cf50538e63612607a7a9447b806556873", + "previous_document_snapshot": [ + { + "path": "docs/私有化软件仓库方案设计.md", + "sha256": "4b30a0e719cfcb4fdbbcd544abb47afefe5b58732f160b997c01e749cca4e1d9" + }, + { + "path": "docs/私有化软件仓库部署说明.md", + "sha256": "5cf4f8396ce47c6dbf42b6e488d8d1e1ed47d3efb1526c56c3fee936f785d27a" + }, + { + "path": "docs/私有化软件仓库测试验收.md", + "sha256": "99100abe4054736b238f20451c925d2e88a3f8594fb00a9b7eb29c55498b67c4" + }, + { + "path": "docs/私有化软件仓库结构化验收报告.md", + "sha256": "e109e0ee4c6692ff2e8e219448fa583cba7f9a522c07dde925791208cd51ce1a" + } + ], + "screenshot_refresh": { + "path": "repository-documentation-screenshots-20261024.json", + "sha256": "e52b8fb9824c7ac7fc06947ed69c8aa961e873743aa992493dc75a3e1c53b78c", + "status": "PASS", + "images": 38, + "core_unchanged": true, + "visual_review": "PASS", + "privacy_status": "PASS" + } +} diff --git a/docs/evidence/repository-final-gate-20261024.json b/docs/evidence/repository-final-gate-20261024.json new file mode 100644 index 0000000000000000000000000000000000000000..afe57d7a571665874c12535754c4480c021465ab --- /dev/null +++ b/docs/evidence/repository-final-gate-20261024.json @@ -0,0 +1,80 @@ +{ + "schema_version": 1, + "date": "2026-10-24", + "scope": "P14 final light gate after genuine full candidate acceptance", + "status": "PASS", + "overall_acceptance": "PASS", + "final_ready": true, + "make_exit_code": 0, + "acceptance_counts": { + "expected_ids": 152, + "unique_expected_ids": 152, + "actual_ids": 152, + "unique_actual_ids": 152, + "duplicate_ids": 0, + "missing_ids": 0, + "unexpected_ids": 0, + "non_pass_ids": 0, + "failed_count": 0, + "passed_count": 152, + "incomplete_evidence_ids": 0 + }, + "core_files_checked": 1467, + "core_unchanged": true, + "core_manifest_sha256": "4b67cf7187fc9262a37d8c3c047188afc296567c52c6fefcfad35339185a5910", + "candidate_source_manifest_sha256": "8e0459690ab021f51d8b8d0d524bad9b7715a1ee203d1dfe7ccfe043f1cd4895", + "documentation": { + "status": "PASS", + "documents": 4, + "local_links_checked": 215, + "acceptance_references_checked": 898, + "missing_references": 0, + "pdf_unchanged": true, + "screenshot_refresh": { + "path": "repository-documentation-screenshots-20261024.json", + "sha256": "e52b8fb9824c7ac7fc06947ed69c8aa961e873743aa992493dc75a3e1c53b78c", + "images": 38, + "local_references_checked": 222, + "old_ui_references": 0, + "core_unchanged": true, + "visual_review": "PASS", + "privacy_status": "PASS" + } + }, + "credential_scan": { + "status": "PASS", + "active_credentials_checked": 7, + "matching_files": 0, + "candidate_files": 1694, + "scan_manifest_sha256": "8ce04acc4e22575bdd6a2d38c1862a59635e4f924aa9e9fa8963aa484b4e0cf2" + }, + "portability_check": { + "exit_code": 0, + "log_sha256": "ab18959287dcbed1db0469fa29ab94fd61f22b55abc85639e7af61fa96b2e0fe" + }, + "runtime": { + "nodes_online": 2, + "architectures": [ + "aarch64", + "x86_64" + ], + "pending_tasks": 0, + "running_tasks": 0, + "historical_failed_tasks_preserved": true + }, + "evidence": [ + { + "name": "p14-document-gate.json", + "sha256": "434935a61f24c2b66ab22d148aca025cf50538e63612607a7a9447b806556873" + }, + { + "name": "credential-scan-summary.json", + "sha256": "990c7f1ba5d3035407ee370ee33d9371f565910d7af919681128e887a5913a10" + }, + { + "name": "runtime-health-recovered.json", + "sha256": "007d8c7e79637f9cf0da01ed53b79dea1b6e1c22a523647bcaf647087e5d2de5" + } + ], + "commit_push_pr_updated": false +} diff --git a/docs/evidence/repository-final-validation-20261024.json b/docs/evidence/repository-final-validation-20261024.json new file mode 100644 index 0000000000000000000000000000000000000000..2a2bac80df708b8d94fa8effda68f8a7e590c027 --- /dev/null +++ b/docs/evidence/repository-final-validation-20261024.json @@ -0,0 +1,401 @@ +{ + "schema_version": 1, + "date": "2026-10-24", + "scope": "Final isolated remediation candidate acceptance after real full entry and document-only P14", + "branch": "codex/repository-review-fixes-20261023", + "base_commit": "05312e2483b7827d07a89a6a29c3437c6a7dd42d", + "commit_push_pr_updated": false, + "final_ready": true, + "overall_acceptance": "PASS", + "quality": { + "repository_tests": 126, + "repository_eslint": "PASS", + "repository_stylelint": "PASS", + "repository_typecheck": "PASS", + "build": "PASS", + "repository_test_files": 20, + "backend_configured_package_tests": { + "packages": 53, + "passed": 529, + "failed": 0, + "opt_in_skipped_in_default_run": 9 + }, + "backend_opt_in": { + "authorization_public_key_large_log": { + "passed": 5, + "skipped": 0 + }, + "worker_mysql_gpg_sync": { + "passed": 4, + "skipped": 0 + }, + "dnf_cases": { + "passed": 3 + }, + "distribution_original_opt_in": "Rerun through full product-reports integration on isolated MySQL; actual distribution to two Agents also passed" + }, + "backend_vet": "PASS", + "backend_build": "PASS" + }, + "verified": { + "mysql_authorization_runs": 5, + "migration_empty_upgrade_reentry": "PASS", + "real_config_repository_tests": 30, + "application_integration_tests": 14, + "dnf_cases": 3, + "tms_services_restarted": 7, + "tms_targets": 2, + "product_restart_redeploy_destroy_restore": "PASS", + "measured_throttle_ratio": 1.0061957483833959, + "mirror_append": "PASS", + "multi_target_install": "PASS", + "dual_architecture_signed_node_rpm": "PASS", + "tms_node_install_auto_registration": "PASS", + "multi_repository_arm_node": "PASS", + "upload_queue_live_http_and_missing_chunk_resume": "PASS", + "deployment_migration_entrypoint": "PASS", + "node_rpm_historical_version_ui_selection": "PASS", + "node_rpm_detached_source_ui_exclusion": "PASS", + "task_manual_scheduled_ui_filter": "PASS", + "current_desktop_layouts": [ + { + "width": 1440, + "height": 900 + }, + { + "width": 1920, + "height": 1080 + } + ] + }, + "security": { + "credential_values_excluded": true, + "runtime_credential_rotation": "PASS", + "note": "External OCAI key and affected HMAC/JWT/MySQL/Redis/ClickHouse credentials replaced. Old authentication was rejected and new credentials accepted in real services. No secret values are included. Existing ZooKeeper anonymous access was not redesigned or described as authenticated.", + "external_ocai_key_rotation": "PASS", + "old_external_key_revoked_confirmed_by_user": true, + "rotation_evidence": { + "path": "repository-credential-rotation-20261024.json", + "sha256": "a3662e8001416ec97e52282ab7c232ebe7318a100239cd99297351096798e020" + }, + "internal_rotation_evidence": { + "path": "repository-internal-authentication-rotation-20261024.json", + "sha256": "e69dc5bdbf0c3c6c9273cede703c48ddb486df9feb15b2efa0ac666a24e4adaa" + }, + "post_rotation_product_evidence": { + "path": "repository-post-rotation-product-validation-20261024.json", + "sha256": "2cc1f95ff32ffda792bdf97ca37865076d85acb15dd6f0b6ab41e4cd3c69a557" + }, + "final_candidate_scan": { + "status": "PASS", + "matching_files": 0, + "active_credentials_checked": 7, + "evidence_sha256": "990c7f1ba5d3035407ee370ee33d9371f565910d7af919681128e887a5913a10" + } + }, + "boundaries": [ + "Historical-version fixture uses current source under an older version number; UI selection is verified, running-node downgrade is not claimed.", + "The backend default suite has opt-in tests; explicitly executed cases and actual product distribution evidence are distinguished.", + "Global pre-existing frontend type diagnostics remain outside the scoped Repository typecheck.", + "Temporary ARM test connectivity is explicit and reversible, not a product default.", + "The 2026-10-24 full entry and runtime credential rotation passed; earlier pre-final limitations are retained only as historical notes.", + "Published UI evidence hides test addresses; original screenshots and local PDF are excluded from the new Git commit." + ], + "evidence": [ + { + "name": "mysql-race-result.json", + "sha256": "a6e5caec712d01f710b3f7f0daf614c528dffe52c9ec9e68802517a9d147d787" + }, + { + "name": "migration-upgrade/final/summary.json", + "sha256": "c3de8c4d21288f7f9f1f227d27512ec035a2ca7bb745e6c31cb11d742c66130c" + }, + { + "name": "presentation-evidence/summary.json", + "sha256": "8196049c23b0d590bd688c13d6c1b1fe38e81143f1f1a822f0a4efabfbafddd3" + }, + { + "name": "product-recovery/summary.json", + "sha256": "f872052e1779c817277c851274367da34f38b2aa5e9e8c1c307efa50f8d92460" + }, + { + "name": "live-tms/summary.json", + "sha256": "4186a8e19b4ba0e6f6e38ba1659673e7fd0d6b24632131e684b654e8b224a82a" + }, + { + "name": "dnf-cases/report.json", + "sha256": "1bc27e0ee064194b51d0a453534efb37a3e9436030abf61613f30bc54e1430c9" + }, + { + "name": "rate-measurement.json", + "sha256": "1a03dc2dea55c71283466b8293a1dafc9ac0c38f407cc3e981959ca118eb4182" + }, + { + "name": "strategy-final.json", + "sha256": "89d4bd3d90e92abf8d500d943fbdc6dbe4ec4a45cb7aa20af1336499dfa750aa" + }, + { + "name": "multitarget-final.json", + "sha256": "b37c4f78e05705882352ffb29a52a3a1d468eaa58fbf406353b7b53b8b75c8eb" + }, + { + "name": "schedule-result.json", + "sha256": "d8ed9e0028b997f21d710269c71fe55023a48b5b963eb4c1575b18c77555c294" + }, + { + "name": "node-rpm-artifacts/build-sign-validation.json", + "sha256": "9aa399657bd31d6097e574318e18b8c00411ca3079c1a7eb5125e9f3c687365c" + }, + { + "name": "node-rpm-x86-install-final.json", + "sha256": "601a168d62296702ff872d85e189f32f8c735fcdd2bc228554eff3a625ec41d0" + }, + { + "name": "node-rpm-arm-install-recovered.json", + "sha256": "e58db39422567901f4e775f769df85a62b96e06754f150fc8d400c7ca1addf7b" + }, + { + "name": "upload-queue-v2-live.json", + "sha256": "a0c742db30a73f407e376ac758309247709dc7542f433e0f25b8a6c8e1dfb5c7" + }, + { + "name": "upload-queue-v2-product-final.json", + "sha256": "d0370dcf0ea0eff6e30d5e6d96a50733ee2bd9aece208cbd1f730a1f5bc7913c" + }, + { + "name": "task-trigger-ui.json", + "sha256": "ce42676c6c47c2671b513609b40791c1b2cf01c14f158b18ef4a32873b8c3adc" + }, + { + "name": "task-trigger-scheduled.png", + "sha256": "94a5584298423b4a2659f2219d8301bf7f3717c3b7532e709e3099dfd893fdf1" + }, + { + "name": "deploy-migration-v1/evidence/summary.json", + "sha256": "34d371a1207d66ffa5af9ff79905a71a29010fbb66011a72c2fcf47d7d078838" + }, + { + "name": "deploy-migration-v1/evidence/deploy-upgrade.log", + "sha256": "d04dd116a4ea29a5e7f6c261b84bd021966217082cc7878acd233ff4beba4505" + }, + { + "name": "deploy-migration-v1/evidence/deploy-reentry.log", + "sha256": "d04dd116a4ea29a5e7f6c261b84bd021966217082cc7878acd233ff4beba4505" + }, + { + "name": "backend-final-presentation-evidence/summary.json", + "sha256": "8905af32e531f898cd21baf2e7761e9b29091b58fefeadc377fac9a78b8b61c9" + }, + { + "name": "backend-final-optin-evidence/summary.json", + "sha256": "0b361f5cecadbe7c7414562a743f2b9946d0e00edf599497a09125e828a37d99" + }, + { + "name": "worker-optin-v3/evidence/summary.json", + "sha256": "0ceebf70ef1a044c5d8483c9582e68e0b863a0c0708b8f7af30bdca77ed4c85b" + }, + { + "name": "schedule-result.json", + "sha256": "d8ed9e0028b997f21d710269c71fe55023a48b5b963eb4c1575b18c77555c294" + }, + { + "name": "mysql-race-result.json", + "sha256": "a6e5caec712d01f710b3f7f0daf614c528dffe52c9ec9e68802517a9d147d787" + }, + { + "name": "backend-final-quality-evidence/summary.json", + "sha256": "97ea35399972144441d9604c3aed59d731b8f955fb51ff580e3c8e0f38817bd1" + }, + { + "name": "worker-optin-v3/default-tests.log", + "sha256": "d256689ef12febd16f3ac699978dd0fa387267c670845070e03c6e64f4514f62" + }, + { + "name": "worker-optin-v3/vet.log", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "worker-optin-v3/build.log", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "node-rpm-arm-install-recovered.json", + "sha256": "e58db39422567901f4e775f769df85a62b96e06754f150fc8d400c7ca1addf7b" + }, + { + "name": "node-rpm-x86-install-final.json", + "sha256": "601a168d62296702ff872d85e189f32f8c735fcdd2bc228554eff3a625ec41d0" + }, + { + "name": "dnf-cases/report.json", + "sha256": "1bc27e0ee064194b51d0a453534efb37a3e9436030abf61613f30bc54e1430c9" + }, + { + "name": "multitarget-final.json", + "sha256": "b37c4f78e05705882352ffb29a52a3a1d468eaa58fbf406353b7b53b8b75c8eb" + }, + { + "name": "upload-queue-v2-product-final.json", + "sha256": "d0370dcf0ea0eff6e30d5e6d96a50733ee2bd9aece208cbd1f730a1f5bc7913c" + }, + { + "name": "framework-final-v1/summary.json", + "sha256": "6847759dbc567938c094e0714d251df30d9404873616806482ec656d2d1c5f30" + }, + { + "name": "node-rpm-historical-v3.json", + "sha256": "f94635888dff71261c59bd97da7dfb788aeed9a9e34ea986ad5b6916659a4dc6" + }, + { + "name": "upload-queue-release/20261024T094348-58d505f2-v3/release-summary.json", + "sha256": "9ba799d1a1da7ff67a9aa67976a8bcee3229b3e56fb193fad6f396aaad5bad57" + }, + { + "name": "worker-optin-v3/changes.json", + "sha256": "c8c3b2323faab91fb81c23a94d8522d772f2a695d9c7c20fbac7c67da1b4b744" + }, + { + "name": "current-v3-task-layout.json", + "sha256": "ae6031875a1056663fb9c58433143ec9fdd3d08fdc510f008173be03a7c7adec" + }, + { + "name": "framework-final-v1/current-strict-v2/validation-summary.json", + "sha256": "16fa6fd97379cdfdd4586bf726f32a5c549ccbc6d15678a16824eb792de09707" + } + ], + "runtime_frontend": { + "status": "PASS", + "candidate_tag": "oc-frontend:review-queue-20261024-v3", + "image_id": "sha256:8f42ad4054d22743fb9cafad1a72860ad91c38583ac37bf6c8e8d9c59465b4d8", + "index_sha256": "ba12be19487b97fc014c8acc2d98fc930f7ed975397fe27630b6f077b7023ea3", + "dist_file_count": 76, + "other_services_unchanged": true, + "all_runtime_hashes_match": true + }, + "current_machine_progress": { + "counts": { + "PASS": 152 + }, + "expected_ids": 152, + "full_release_gate": "PASS", + "make_exit_code": 0, + "final_ready": true + }, + "updated_at": "2026-10-24T11:49:19.070Z", + "strict_result_validation": { + "exit_code": 0, + "passed": true, + "counts": { + "expected_ids": 152, + "unique_expected_ids": 152, + "actual_ids": 152, + "unique_actual_ids": 152, + "duplicate_ids": 0, + "missing_ids": 0, + "unexpected_ids": 0, + "non_pass_ids": 0, + "failed_count": 0, + "passed_count": 152, + "incomplete_evidence_ids": 0 + }, + "scope": "Actual strict stage executed by make repo-acceptance on the frozen candidate", + "evidence_sha256": "8e2f5514a3ac33b9eee0f460fe135643c6fd0f2a892880101fd3024cb6cc2782" + }, + "progress_manifest": { + "path": "repository-acceptance-progress-20261024.json", + "sha256": "a7a40f874ffa0ff344f486c44853226fe831c49a9aa2db6843b0da89c70a59eb", + "source_raw_results_sha256": "7ecd70b3939728b836dd5480b2cace28f032caebafbed81b2bb5dfb7432fb44e" + }, + "runtime_backend": { + "status": "PASS", + "image_id": "sha256:81fef43c3aad14931a52516be2e0ea71299833357ec7d58f8a95fdf4a7b5027f", + "binary_sha256": "f883372ce66abc51926915eee765526ef2b236c7df0d42693854d1ada511bcf9", + "scope": "Central error catalog mapping; system repository protection remains enforced", + "evidence": { + "path": "repository-node-rpm-error-validation-20261024.json", + "sha256": "ea24be449c7c515506a9fe3ac305768bda65b5ac8777d256a54343208b6ea6c3" + } + }, + "task_traceability_evidence": { + "path": "repository-taskflow-validation-20261024.json", + "sha256": "8007679b70872ce894642c3b1da66d99a39a489a521b8da27465952f6b2493eb" + }, + "complete_component_gate": { + "legacy_passed": 31, + "bundles_passed": 9, + "all_execution_step_exits_zero": true, + "final_152_make_pending": false, + "evidence": { + "path": "repository-release-validation-20261024.json", + "sha256": "81b77bd5a86ad204e9ac360b024c9aafef1d70859ff75d5cb69d5f71c665abf8" + } + }, + "documentation_evidence": { + "path": "repository-documentation-validation-20261024.json", + "sha256": "bb830ea500eac1e5a57b75010a4b5749105c55e9eadd3ec64b2258f76d3147be" + }, + "action_evidence": { + "path": "repository-action-traceability-20261024.json", + "sha256": "5a419bc2e56dafb5e8f1401b2e9f94cd1590a7d693923c92bd772ccdf7ed7cc7" + }, + "test_portability_evidence": { + "path": "repository-test-portability-validation-20261024.json", + "sha256": "27dc1d958b08e3d73740c253350f50fde6335bbfcc6e1337d3c5e576d09fc6eb" + }, + "previous_incomplete_strict_result_validation": { + "exit_code": 1, + "passed": false, + "counts": { + "expected_ids": 152, + "unique_expected_ids": 152, + "actual_ids": 152, + "unique_actual_ids": 152, + "duplicate_ids": 0, + "missing_ids": 0, + "unexpected_ids": 0, + "non_pass_ids": 8, + "failed_count": 0, + "passed_count": 144, + "incomplete_evidence_ids": 0 + }, + "scope": "Earlier incomplete 144-item result validation retained as historical evidence; not the new candidate result", + "historical": true + }, + "candidate": { + "source_manifest_sha256": "8e0459690ab021f51d8b8d0d524bad9b7715a1ee203d1dfe7ccfe043f1cd4895", + "frontend_lockfile_sha256": "cf3f0c0dd9eadf8fe2358241d44e2ec087a20ba0f72d288a51013b7f42ead0ea", + "p14_core_files_checked": 1467, + "p14_core_unchanged": true, + "tested_result_sha256": "7ecd70b3939728b836dd5480b2cace28f032caebafbed81b2bb5dfb7432fb44e" + }, + "p14": { + "status": "PASS", + "documentation": true, + "links_images": true, + "acceptance_manifest_complete": true, + "pdf_unchanged": true, + "commit_push_pr_updated": false, + "final_light_gate": { + "path": "repository-final-gate-20261024.json", + "sha256": "23390604f11a5f310054eee788890e43db3930c24c72fe1ccb3ed8e5268e8811" + }, + "screenshot_refresh": { + "path": "repository-documentation-screenshots-20261024.json", + "sha256": "e52b8fb9824c7ac7fc06947ed69c8aa961e873743aa992493dc75a3e1c53b78c", + "scope": "Documentation and screenshot update only; original full acceptance unchanged" + } + }, + "historical_pre_final_boundaries": [ + "Final repo-acceptance aggregate gate has not passed; current partial manifest is not final acceptance.", + "Runtime credential rotation remains pending; no secret values are included in public evidence." + ], + "publication": { + "date": "2026-10-25", + "privacy_status": "PASS", + "screenshots_redacted": true, + "local_pdf_excluded": true, + "approved_baseline_preserved": true, + "source_branch": "codex/repository-review-fixes-20261023", + "existing_pr": "https://gitee.com/OpenCloudOS/ocmanager/pulls/420" + } +} diff --git a/docs/evidence/repository-internal-authentication-rotation-20261024.json b/docs/evidence/repository-internal-authentication-rotation-20261024.json new file mode 100644 index 0000000000000000000000000000000000000000..e3b68647bcdef805bd643fe1de0c6dc375631015 --- /dev/null +++ b/docs/evidence/repository-internal-authentication-rotation-20261024.json @@ -0,0 +1,76 @@ +{ + "schema_version": 1, + "date": "2026-10-24", + "scope": "Internal TMS HMAC, shared JWT, MySQL, Redis and ClickHouse authentication rotation. Separate from external OCAI rotation and full acceptance.", + "internal_authentication_rotation": "PASS", + "final_ready": false, + "overall_acceptance": "NOT_PASS", + "tms": { + "new_hmac_signed_http_status": 200, + "old_hmac_signed_http_status": 401, + "strict_retained": true + }, + "jwt": { + "runtime_values_replaced": true, + "old_token_rejected": true, + "http_status": 200, + "error_code": "InternalError", + "explicit_signature_error_observed": false, + "limitation": "The old-token API request failed, but the response did not identify signature rejection explicitly." + }, + "mysql": { + "old_root_rejected": true, + "old_application_rejected": true, + "new_root_and_application_accepted": true + }, + "redis": { + "old_rejected": true, + "new_accepted": true, + "persisted_test_marker_preserved": true + }, + "clickhouse": { + "xml_user_storage_verified": true, + "old_rejected": true, + "new_direct_and_distributed_queries_accepted": true + }, + "data_and_artifacts": { + "repository_task_revision_fingerprint_unchanged": true, + "images_mounts_volumes_unchanged": true, + "all_services_healthy": true + }, + "zookeeper": { + "authentication": "NOT_ENABLED", + "rotation": "NOT_APPLICABLE_TO_EXISTING_UNUSED_PASSWORD_VARIABLE", + "note": "No ZooKeeper SASL authentication was enabled by this change. Do not interpret the unused password variable as an authentication control." + }, + "safeguards": { + "credential_values_excluded": true, + "private_runtime_backups_retained": true, + "no_private_backup_in_public_evidence": true, + "no_commit_push_or_pr_update": true + }, + "external_ocai_rotation": { + "path": "repository-credential-rotation-20261024.json", + "sha256": "a3662e8001416ec97e52282ab7c232ebe7318a100239cd99297351096798e020", + "note": "Historical external-key stage report; its internal-rotation PENDING field records that earlier stage, not the latest combined state." + }, + "evidence": [ + { + "name": "authentication-rotation-summary.json", + "sha256": "781042af9134e324114fe5721ee0bac27401395dc8c89686edd0361d4eaadf32" + }, + { + "name": "old-jwt-rejection.json", + "sha256": "ab35723c305ef8da3c9ead53d968a8208cbc2cf6a9eb6db49eb34b2433b7b98f" + }, + { + "name": "storage-rotation-summary.json", + "sha256": "ded7efdc2d719bfd5ba32ef0f3920df476fbff219b148a6e84db5e16929b8e2f" + } + ], + "boundaries": [ + "This summary proves the recorded authentication and persistence checks; final candidate acceptance remains unfinished.", + "Post-rotation user login, Repository APIs, node heartbeats and genuine TMS execution must be checked separately.", + "Protected historical backups are retained outside the repository; this is not a claim that every historical private copy has been erased." + ] +} diff --git a/docs/evidence/repository-node-rpm-error-validation-20261024.json b/docs/evidence/repository-node-rpm-error-validation-20261024.json new file mode 100644 index 0000000000000000000000000000000000000000..fedd8ce8dd7a841ab8233cbcc606bfeebfa7388a --- /dev/null +++ b/docs/evidence/repository-node-rpm-error-validation-20261024.json @@ -0,0 +1,172 @@ +{ + "schema_version": 1, + "date": "2026-10-24", + "scope": "Central Repository Node RPM error mapping and actual deployed negative API checks; not full acceptance.", + "mapping_status": "PASS", + "final_ready": false, + "overall_acceptance": "NOT_PASS", + "source_files": [ + { + "path": "manager/backend/services/footstone/domain/repositoryErrorCatalog/catalog.go", + "sha256": "511e474e201c87b10be5e1e4e3afe09888ed01ab2069fba5c007973de5220475" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryErrorCatalog/catalog_test.go", + "sha256": "e0559d74b09db414d0c595890cb59f180977fbd97f1cf2ae49353cbddda92f95" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_error_catalog.go", + "sha256": "fc90387d66373c76522b089998e0dea63980e8e067b6129dc1989303bdd52ff9" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_error_catalog_test.go", + "sha256": "2d769215b0d793d81d1eef3360ac15012de4c90156d50eb241ce5395603ba826" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_controller.go", + "sha256": "009fa79487324e00aad507d20dcd55c6483b7b8c0cd0e6d4b5e5c28f1656841a" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_content_controller.go", + "sha256": "4f435dade49b279f9460b84f04989318b32ac0226466433acb8981fcf2546e89" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_node_controller.go", + "sha256": "4f74112e54c9c34926dcde72495abeca63416443f381fbc024ab81084386faab" + } + ], + "quality_checks": [ + { + "name": "catalog", + "command": [ + "go", + "test", + "-json", + "-count=1", + "./services/footstone/domain/repositoryErrorCatalog" + ], + "exit_code": 0, + "seconds": 4.376, + "tests": { + "pass": 6, + "fail": 0, + "skip": 0 + } + }, + { + "name": "api-error-mapping", + "command": [ + "go", + "test", + "-json", + "-count=1", + "-run", + "^TestRepository(NodeRPMErrors|NodeRPMErrorMapping|ErrorCatalog)", + "./services/footstone/presentation" + ], + "exit_code": 0, + "seconds": 26.518, + "tests": { + "pass": 10, + "fail": 0, + "skip": 0 + } + }, + { + "name": "vet", + "command": [ + "go", + "vet", + "./services/footstone/domain/repositoryErrorCatalog", + "./services/footstone/presentation" + ], + "exit_code": 0, + "seconds": 9.504 + }, + { + "name": "build", + "command": [ + "go", + "build", + "./..." + ], + "exit_code": 0, + "seconds": 12.267 + } + ], + "deployed_artifact": { + "image_id": "sha256:81fef43c3aad14931a52516be2e0ea71299833357ec7d58f8a95fdf4a7b5027f", + "binary_sha256": "f883372ce66abc51926915eee765526ef2b236c7df0d42693854d1ada511bcf9", + "healthy": true, + "schema_changed": false, + "protection_relaxed": false + }, + "actual_api": { + "cases": [ + { + "label": "protected_sync", + "http_status": 200, + "success": false, + "error_code": "REPOSITORY_NODE_SYNC_FORBIDDEN", + "error_message": "Node RPM 安装源仅接收私有上传,不允许远程同步。 建议:通过上传和发布流程更新节点 RPM,不要为系统安装源配置外部上游。 技术原因:system-managed Node RPM repository cannot use remote synchronization" + }, + { + "label": "protected_delete", + "http_status": 200, + "success": false, + "error_code": "InternalError", + "error_message": "repository deletion requires explicit confirmation" + }, + { + "label": "artifact_missing", + "http_status": 200, + "success": false, + "error_code": "REPOSITORY_NODE_ARTIFACT_NOT_FOUND", + "error_message": "当前安装源没有可用且匹配目标架构的节点 RPM 版本。 建议:刷新版本列表并选择当前启用安装源中的已发布版本,或先上传、发布并登记对应架构的节点 RPM。 技术原因:repository node artifact not found" + }, + { + "label": "protected_delete_confirmed", + "http_status": 200, + "success": false, + "error_code": "REPOSITORY_SYSTEM_MANAGED", + "error_message": "该仓库用于系统组件安装,不能通过普通仓库操作修改或删除。 建议:保留受保护的 Node RPM 仓库;需要调整安装来源时,使用仓库节点页面的安装源配置。 技术原因:system-managed repository cannot be changed through the standard repository API" + }, + { + "label": "artifact_architecture_mismatch", + "http_status": 200, + "success": false, + "error_code": "REPOSITORY_NODE_ARTIFACT_NOT_FOUND", + "error_message": "当前安装源没有可用且匹配目标架构的节点 RPM 版本。 建议:刷新版本列表并选择当前启用安装源中的已发布版本,或先上传、发布并登记对应架构的节点 RPM。 技术原因:repository node artifact not found" + } + ], + "task_count_before": 128, + "task_count_after": 128, + "no_rejected_operation_created_a_task": true + }, + "behavior": { + "authoritative_error_catalog_reused": true, + "english_code_preserved": true, + "chinese_reason_and_action_for_mapped_node_rpm_errors": true, + "original_technical_cause_preserved": true, + "system_repository_protection_retained": true, + "no_schema_or_proto_change": true + }, + "limitations": [ + "The request without explicit delete confirmation still returns InternalError and an English confirmation-required message. This is not counted as a translated Node RPM protection error.", + "These targeted checks do not independently close every error-display or 152-item acceptance criterion." + ], + "evidence": [ + { + "name": "manifest-audit/summary.json", + "sha256": "2c9e313362e1e6ae81ea1a503f197c02b2a80e3deb114bf05f7c9ccb407d66d8" + }, + { + "name": "footstone-error-catalog-deploy/summary.json", + "sha256": "bc80952821361e607831e66791ef3936b4489ddb2a5454109bec22cc43ffeb16" + }, + { + "name": "api-error-mapping-runtime.json", + "sha256": "cb74f6b0359ae16e6320b30c3d394b16bfbc76cac3e2a3e04e519c16e8591c58" + } + ] +} diff --git a/docs/evidence/repository-post-rotation-product-validation-20261024.json b/docs/evidence/repository-post-rotation-product-validation-20261024.json new file mode 100644 index 0000000000000000000000000000000000000000..b0f467aa6addd27d90057dc53ba1995363e16bb9 --- /dev/null +++ b/docs/evidence/repository-post-rotation-product-validation-20261024.json @@ -0,0 +1,60 @@ +{ + "schema_version": 1, + "date": "2026-10-24", + "scope": "Post-authentication-rotation product API and actual OCAI analysis checks; not complete acceptance", + "status": "PASS", + "final_ready": false, + "overall_acceptance": "NOT_PASS", + "node_api": { + "success": true, + "nodes": [ + { + "architecture": "aarch64", + "status": "ONLINE", + "agent_version": "0.2.2-0.review20261024.1.oc9" + }, + { + "architecture": "x86_64", + "status": "ONLINE", + "agent_version": "0.2.2-0.review20261024.1.oc9" + } + ] + }, + "ocai": { + "analysis_completed": true, + "context_api_success": true, + "repository_task_count_before": 128, + "repository_task_count_after": 128, + "no_new_repository_mutation_task": true, + "scope": "Analysis completed using a real failed task. No Repository mutation task was created by this check." + }, + "inventory_import": { + "global_task_detail_success": true, + "task_type": 12, + "task_status": "SUCCESS", + "scope": "Queries an existing successful INVENTORY_IMPORT task; this check does not claim a new import execution." + }, + "probe_limitation": { + "post_completion_dialog_read_only_text_detected": false, + "note": "The dialog was already closed when the text predicate ran. The false predicate is preserved and is not used as evidence of an executed mutation. API context, completion state and before/after task counts are reported separately." + }, + "values_excluded": true, + "evidence": [ + { + "name": "post-rotation-nodes.json", + "sha256": "bc6b476aeff02eb0c3adc607a9a02e0d01208c65146973eb587f4d3adfc7150b" + }, + { + "name": "post-rotation-product-checks.json", + "sha256": "455357fcb660113900194fe7833c30d1542d532a9e4a47b082c397e514869293" + }, + { + "name": "post-rotation-ocai.json", + "sha256": "7e451487994984b4d0e3fb3c8efbb87acef44b31cfcfefe5336c7b7f261dcc25" + }, + { + "name": "post-rotation-ocai.png", + "sha256": "bb55558cda534eb2d7d189cdd68a4c72a41f59c4750d672bdee15566185f3cbd" + } + ] +} diff --git a/docs/evidence/repository-publication-privacy-20261025.json b/docs/evidence/repository-publication-privacy-20261025.json new file mode 100644 index 0000000000000000000000000000000000000000..92b70d60b5ad445d89034d97ff7925e6b3316054 --- /dev/null +++ b/docs/evidence/repository-publication-privacy-20261025.json @@ -0,0 +1,242 @@ +{ + "schema_version": 1, + "date": "2026-10-25", + "scope": "Publication privacy check; not a replacement of full acceptance results", + "status": "PASS", + "changed_screenshots": 44, + "new_scene_images": 38, + "historical_images_masked": 6, + "original_screenshots_included": false, + "local_pdf_included": false, + "vm_address_masks_applied": true, + "credentials_included": false, + "business_code_changed": false, + "full_acceptance": { + "date": "2026-10-24", + "passed": 152, + "expected": 152, + "source": "repository-final-validation-20261024.json" + }, + "images": [ + { + "path": "docs/images/repository/repository-current-20261024-columns.png", + "sha256": "fa98e662f7df2c50d0d0d172fb580f9c3753f7293a70463a8964168002c98b49", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-distribution.png", + "sha256": "4379950b2ae8e2ff8c038b7d1682c641cfce1ddfbe81d0f37c9aac8634fe11a3", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-node-drawer.png", + "sha256": "7d442ea24f035d672c301d337331b39ff44109ea679f7cfb3bb78531eb44ce83", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-package-install-preview.png", + "sha256": "a35b3e9136e45b0bf6154621aaed3cb145d56739bad3dc603e9591dcec239cc1", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-package-install-result.png", + "sha256": "af59f7fbf6161c5a916a9cdf38bd4afe308618d1850f6f6e797d41accf2ab0f2", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-batch-delete-preview.png", + "sha256": "27cfe29d1d128e38a5862a1cd459c9933be678c6f8af23c5296fb77552dbeee3", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-batch-delete-result.png", + "sha256": "072665209ec8c51e32532680f85da3a5b0147f2cc0ee67e97ce51665f0ae4ae1", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-inventory-import-preview.png", + "sha256": "26617aaf4d968d826798e4d0513baf0d8454d80beb373fa5d4e7373910ff1d80", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-inventory-import-task.png", + "sha256": "09d3fb03b673c49d594c991031b2d744fddb3c934ea8668eb4fe16ab7ad724f3", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-create-noarch-filter.png", + "sha256": "0165ac2bc0ebaffc6c0e6d1d6aab3e82f327dbf763a1f617e2f3e4ee36951abe", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-sources.png", + "sha256": "5f941c710384455832cb3fc652cfbbd41849ac3f0f717f6102ec516515d5d5ed", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-create-upstream.png", + "sha256": "1b6824a0f58c9f5a0e0acc2b24f6e8aeb24d5ab9d8ca36b1a3b6748924030ad0", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-create-filter.png", + "sha256": "a3d369d745d37db4ee0879bc03e2f7c0a5bdda5f0af33991999ebbae9c3eea7d", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-list.png", + "sha256": "fdbce9214d5e6231affccb216d8747f6b5ffa0f8fb710e925914846a0b1ebf90", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-nodes.png", + "sha256": "12887dfe3b8c7c4591d339fa097ffc5f718122f7d5f7cf8f9c942cf5d0e0c556", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-node-install.png", + "sha256": "170f02c1032680f088a97c067030d472891672b44bebd2da15c6bb1bfce661e7", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-node-install-sources.png", + "sha256": "5e654538831a51d0f53113a4bd77e2853ed93925a8e3d413e9d06e25871ee592", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-node-allowlist.png", + "sha256": "2c8eacbbcffa17771796c5755295cff4a7aadfef8a4122e173656fe335bad4ed", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-overview.png", + "sha256": "1a1ce162e42dd52c8f71ed7d500a80fe0e8c680af87e9bc7a3b66d26309f8986", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-packages.png", + "sha256": "0d592f6564b0853d3cb92af4661444ce83213be3cb0a42f3f25ae453baa8a09e", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-package-install-nodes.png", + "sha256": "493eafaae0f6b2418957030b71e9bb798e818b24dcda8b6afd1f718bd7bcb526", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-upload-queue.png", + "sha256": "56ea1498804ca6b9ca13d766a7fbe023faa8c3097a94bcbff24a1438038007f0", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-quarantine.png", + "sha256": "b1fa6bcff2688a65a6f200acedd387a1ab370c0c05a5411fa3759acbcf84733a", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-tasks.png", + "sha256": "f4be26776962a65e2b3494e0fa694c44315d3e200965a5b12c86c9875aed0bed", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-task-failure.png", + "sha256": "a014673bf0f9286f772b10fe098578c4acc6131ab20b1f7084e115afc4896198", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-ocai-result.png", + "sha256": "24adfb941197373c9d6ad5613e1f7695fb1fa15be9705a6e7b306a9a77ed777c", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-config.png", + "sha256": "f88f032da5562d7873add469292283c1688e77227d557ea3fa726512dae9e051", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-gpg.png", + "sha256": "c04ad6639a0308dbce7eccc52d7b4411ef41226abe8e7df748f5d869d5c6ee46", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-distribution-detail.png", + "sha256": "56a417d0666024c2b14ce25874991ed0cb363bd5f0e617f5219e3dd6759104b5", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-source-form.png", + "sha256": "f9d9244346d8cf30669fb81e93644fd151db6048d37510cfff1da4b0efcb5e41", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-ocai-context.png", + "sha256": "97f48640be09a41187ee43c81b1958c8571eb6f93284e437572efbbe1ffb25f3", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-batch20-packages.png", + "sha256": "1f669953588caf59af4853ea1b019b6d9c3b31abbd67fffaf991d00738c18f22", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-batch20-distribution-detail.png", + "sha256": "56a417d0666024c2b14ce25874991ed0cb363bd5f0e617f5219e3dd6759104b5", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-arm-node-drawer.png", + "sha256": "06ce9022aca0c477d6a93c7e8acc44dfe2a90a64a96d7112de9d898426d97978", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-command-helper-rerun.png", + "sha256": "ff2b62e5b6b319bdc573c11374c321d9e6a1851980e3afab9fe344cc24a9ca81", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-command-helper-preview.png", + "sha256": "f47c511db5800ef17942f1eecac7063635c2b5f63ea5258ccc9e5b113b43c60c", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-disk-chart.png", + "sha256": "389d197a1be6ac89f080a0cf88919950d74dda652f7231974edef1f5750b715b", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-current-20261024-task-success.png", + "sha256": "af59f7fbf6161c5a916a9cdf38bd4afe308618d1850f6f6e797d41accf2ab0f2", + "method": "Live Playwright screenshot with test-address masks; no mutation confirmation" + }, + { + "path": "docs/images/repository/repository-final-20261024-node-version.png", + "sha256": "7c989e84a227906e99dccc909a20191232c34bf1ce433cf332a2ed8499cb4313", + "method": "Original historical screenshot with opaque test-address masks; experiment data unchanged" + }, + { + "path": "docs/images/repository/repository-review-20261023-optical.png", + "sha256": "d56d88eb99265b3ae2053ac6193021ae8d51dff2b7bd8f7acb46263c30dd1d71", + "method": "Original historical screenshot with opaque test-address masks; experiment data unchanged" + }, + { + "path": "docs/images/repository/repository-style-20261023-distribution.png", + "sha256": "3f1fadf0c58264eb7d701d1e946a46777e81fa6cf84601bdf2ab2198c6b0354c", + "method": "Original historical screenshot with opaque test-address masks; experiment data unchanged" + }, + { + "path": "docs/images/repository/repository-style-20261023-list.png", + "sha256": "2ae4920b1102fd69f56bc6e676c782c0f05314f0c1d200151f2a2e4c6e285df9", + "method": "Original historical screenshot with opaque test-address masks; experiment data unchanged" + }, + { + "path": "docs/images/repository/repository-style-20261023-node-drawer.png", + "sha256": "f6a051f2ee861f0bc2d645aaa3fe1cb0cadbe8598d9aaee83dd2d9c78f04c4f2", + "method": "Original historical screenshot with opaque test-address masks; experiment data unchanged" + }, + { + "path": "docs/images/repository/repository-style-20261023-nodes.png", + "sha256": "59b4dc2f4b1d8f2dce8e37294db2e72d418f8ece7a00fd6410db32844c60f662", + "method": "Original historical screenshot with opaque test-address masks; experiment data unchanged" + } + ] +} diff --git a/docs/evidence/repository-recovery-validation-20261024.json b/docs/evidence/repository-recovery-validation-20261024.json new file mode 100644 index 0000000000000000000000000000000000000000..7c371c4e029d5b34aab14a6d2e6dfb1e5dcf6953 --- /dev/null +++ b/docs/evidence/repository-recovery-validation-20261024.json @@ -0,0 +1,98 @@ +{ + "schema_version": 1, + "date": "2026-10-24", + "scope": "controlled cancellation and deployed service recovery", + "approved_commit": "05312e2483b7827d07a89a6a29c3437c6a7dd42d", + "branch": "codex/repository-review-fixes-20261023", + "commit_push_pr": false, + "repository_id": "58e29f78-fa29-4118-80fd-befd8ff30b3a", + "findings": { + "root_cause": "canceled CommandContext returned process-killed error; log FAILED did not match Worker CANCELED report", + "before": { + "task_id": "aac8cc60-f187-41f9-b002-c72b522a2509", + "status": 4, + "code": "CANCELED_AFTER_LEASE_EXPIRY" + }, + "fix": { + "files": [ + "manager/backend/services/footstone/repositoryworker/logging_executor.go", + "manager/backend/services/footstone/repositoryworker/logging_executor_test.go" + ], + "worker_sha256": "23b3dca79e8f43965fdee2f221a4702ffe89d427bdfa95dd3bb45e4478fad55e", + "old_binary_backed_up": true + } + }, + "cancellation": { + "status": "PASS", + "task_id": "b8840b02-df8d-48f4-b23a-a36ef470ffc9", + "error_code": "CANCELED_BY_USER", + "seconds": 10.228, + "following_sync_task": "7f287942-f01e-42c5-99f9-30b49e03c157", + "following_sync_status": 5 + }, + "control_outage": { + "status": "PASS", + "task_id": "02419b81-fce7-468e-90fd-00e8b9b03c81", + "stopped_seconds": 25, + "worker_pid_unchanged": true, + "attempt_before": 1, + "attempt_after": 2, + "fencing_before": "6", + "fencing_after": "7", + "revision": "d1c3d014-02d3-44eb-8404-0037270c1dcd" + }, + "worker_restart": { + "status": "PASS", + "task_id": "64a62ead-ea15-42bf-b7bf-0f565d13adee", + "attempt_before": 1, + "attempt_after": 2, + "fencing_before": "8", + "fencing_after": "9", + "revision": "adb4ee04-4650-49f3-8b45-ada05df28673" + }, + "tests": { + "worker_go_test": "PASS", + "worker_go_vet": "PASS", + "added_cases": 2 + }, + "final": { + "node_online": true, + "no_active_tasks": true, + "fixture_server_stopped": true, + "history_retained": true + }, + "evidence": [ + { + "name": "product-timeline-final.json", + "sha256": "ba735014f4c8bf7e98d17696a946af9239c8a528f546efa5bdc1f2a30c0c90b5" + }, + { + "name": "worker-journal.log", + "sha256": "37eeeb8f693d2aad6318aea5ad1ae72c42fee4963ea74c3b9c89a124c6f8a894" + }, + { + "name": "runtime-final.txt", + "sha256": "4c30768c0168a358b039c3a4a955d54009e7747cdac892671ebf25ed432af731" + }, + { + "name": "source-requests.jsonl", + "sha256": "76f38f88af35ff21020f64ed57315415e9e4517fc00d5b7e1eb94dd5b2a099d5" + }, + { + "name": "worker-test.log", + "sha256": "f79ac5c78b762527c5f0ce037a622b5909254d38a5f0ad20bba015e8c6f591d7" + }, + { + "name": "worker-vet.log", + "sha256": "7bfed9c2aee684312ee243f8ee4ed35268a37caac09a81bb970b96d02dafadad" + } + ], + "full_152_rerun": false, + "final_ready": false, + "not_covered": [ + "aarch64", + "Node RPM allowlist race", + "destroy/restore", + "old fencing forged report injection" + ] +} diff --git a/docs/evidence/repository-release-validation-20261024.json b/docs/evidence/repository-release-validation-20261024.json new file mode 100644 index 0000000000000000000000000000000000000000..c8318453487d5d46bfa77b2fb3d917600450b123 --- /dev/null +++ b/docs/evidence/repository-release-validation-20261024.json @@ -0,0 +1,83 @@ +{ + "schema_version": 1, + "date": "2026-10-24", + "scope": "Actual full make repo-acceptance with all 152 criteria on frozen candidate, followed by document-only P14 checks", + "status": "PASS", + "legacy_criteria": { + "passed": 31, + "failed": 0, + "blocked": 0, + "pending": 0, + "total": 31 + }, + "bundles": { + "categories": 9, + "passed": 9, + "failed": 0, + "blocked": 0, + "pending": 0 + }, + "recovery_checks": { + "restart": "PASS", + "redeploy": "PASS", + "destroy_restore": "PASS", + "tms_restart": "PASS", + "acceptance_after_restore": "PASS", + "evidence_bundles": "PASS" + }, + "candidate_source_manifest_sha256": "8e0459690ab021f51d8b8d0d524bad9b7715a1ee203d1dfe7ccfe043f1cd4895", + "source_postcheck": true, + "evidence": [ + { + "name": "release-acceptance/summary.json", + "sha256": "f87c2fa573fb3028a5d717749685c2d6b9da9773c6709bdc01fa043d0477df66" + }, + { + "name": "bundles/summary.json", + "sha256": "b21de763f47dadb7f00f606aa05c5e69e4d264a846197d7e759a60fde16c0139" + }, + { + "name": "recovery/summary.json", + "sha256": "1a99dc89a1365332aaeece1f75fa282e72227804f181b0e83a187044fae01f44" + }, + { + "name": "controller-summary.json", + "sha256": "2663bee1a500f997204f88cc8e35547daf7ccb1861caf2ea6dcffbfa4f1b73c7" + }, + { + "name": "results-152/validation-summary.json", + "sha256": "8e2f5514a3ac33b9eee0f460fe135643c6fd0f2a892880101fd3024cb6cc2782" + }, + { + "name": "full-entry.log", + "sha256": "78bea4bac00c7c39c31767da69f6a6c84c44e49780c73b12a1856d85a4496a73" + }, + { + "name": "export-manifest.json", + "sha256": "dd09223b00a43489cbb5c3bd882e5884beaf2640a0eb03242a37d03cf2b9b502" + } + ], + "final_ready": true, + "overall_acceptance": "PASS", + "make_exit_code": 0, + "strict_counts": { + "expected_ids": 152, + "unique_expected_ids": 152, + "actual_ids": 152, + "unique_actual_ids": 152, + "duplicate_ids": 0, + "missing_ids": 0, + "unexpected_ids": 0, + "non_pass_ids": 0, + "failed_count": 0, + "passed_count": 152, + "incomplete_evidence_ids": 0 + }, + "p14": { + "core_files_checked": 1467, + "core_unchanged": true, + "document_links_checked": 215, + "pdf_unchanged": true, + "document_gate_sha256": "434935a61f24c2b66ab22d148aca025cf50538e63612607a7a9447b806556873" + } +} diff --git a/docs/evidence/repository-review-fixes-20261023.json b/docs/evidence/repository-review-fixes-20261023.json new file mode 100644 index 0000000000000000000000000000000000000000..ba90c9136249f35eed52c5dddfd84ab18c516014 --- /dev/null +++ b/docs/evidence/repository-review-fixes-20261023.json @@ -0,0 +1,409 @@ +{ + "schema_version": 1, + "date": "2026-10-23", + "approved_commit": "05312e2483b7827d07a89a6a29c3437c6a7dd42d", + "branch": "codex/repository-review-fixes-20261023", + "approved_remote_and_pr_unchanged": true, + "commit_push_executed": false, + "status": "IMPLEMENTED_TARGETED_VALIDATED", + "production_replacement_executed": false, + "full_152_acceptance_rerun": false, + "final_ready": false, + "quality": { + "repository_tests": { + "files": 16, + "passed": 105 + }, + "operations_tests": { + "passed": 42, + "failed": 0 + }, + "checks": [ + { + "id": "backend-tests", + "exit_code": 0, + "duration_ms": 20955 + }, + { + "id": "backend-vet", + "exit_code": 0, + "duration_ms": 6447 + }, + { + "id": "backend-build", + "exit_code": 0, + "duration_ms": 18021 + }, + { + "id": "repository-tests", + "exit_code": 0, + "duration_ms": 14009 + }, + { + "id": "adjacent-ui-tests", + "exit_code": 0, + "duration_ms": 29740 + }, + { + "id": "repository-typecheck", + "exit_code": 0, + "duration_ms": 19995 + }, + { + "id": "new-ui-eslint", + "exit_code": 0, + "duration_ms": 8945 + }, + { + "id": "frontend-build", + "exit_code": 0, + "duration_ms": 57043 + } + ], + "shell_contracts": { + "environment": "OpenCloudOS 9.6", + "exit_code": 0, + "evidence_sha256": "b7ecf5eb8c1411077b326c3e173977f06c985209749385cf2a2c84518b68e2df" + }, + "initial_environment_failure": { + "id": "shell-contracts", + "environment": "WSL Ubuntu 24.04", + "cause": "jq unavailable", + "exit_code": 1, + "raw_log_preserved": true + } + }, + "real_validation": { + "mysql": { + "main_tests": 3, + "upload_subtests": 2, + "temporary_db_and_user_removed": true + }, + "transport": { + "wsl_repetitions": 20, + "oc9_repetitions": 10, + "real_tcp_mtls": true, + "task_executor": "fixture" + }, + "gpg": { + "os": "OpenCloudOS 9.6", + "dnf": "4.16.2", + "rpm": "4.18.2", + "preview_target_package_missing": true, + "preview_rpmdb_unchanged": true, + "wrong_metadata_key_rejected": true, + "confirm_install": true, + "empty_installroot": { + "packages_installed": 19, + "dnf_exit_code": 0, + "rpm_query_exit_code": 0, + "gpgcheck": 1, + "repo_gpgcheck": 1, + "default_repositories_disabled": true + } + }, + "browser": { + "viewports": [ + "1440x900", + "1920x1080" + ], + "rerun_parameter_target_restore": true, + "command_preview": true, + "command_execution": false, + "raw_optical_usage_percent": 100, + "optical_filtered_from_kpi_and_trend": true + } + }, + "limitations": [ + "No replacement of the approved Footstone or Worker runtime images.", + "Successful upload retry integration supplies accepted-result metadata; it does not reproduce every browser upload and RPM/GPG/repodata step.", + "HMAC production switch evidence predates this handover; tests were rerun without issuing another production command.", + "Additional transport race run aborted in upstream tnet checkptr; the failure is preserved and checkptr was not disabled.", + "Existing large-chunk warning and historical adjacent SFC lint diagnostics remain.", + "Targeted fixes are not a new 152-item full acceptance result." + ], + "screenshots": [ + { + "path": "docs/images/repository/repository-review-20261023-rerun.png", + "sha256": "78d58418d76751db77d44e6f26bc74a901edbf0eceee9d81b20669ceaea914b4" + }, + { + "path": "docs/images/repository/repository-review-20261023-command-preview.png", + "sha256": "3db3cea8731b1dcd7d2ef3f64a9ff2ef309fcd8b0af9f1cc12ba11f269b44193" + }, + { + "path": "docs/images/repository/repository-review-20261023-optical.png", + "sha256": "d56d88eb99265b3ae2053ac6193021ae8d51dff2b7bd8f7acb46263c30dd1d71", + "original_sha256": "d2ab3ff3e9ebe1a2902a5cd82861196380c6294afeaae6969d67b932981bd7e5", + "address_redacted": true + } + ], + "results": [ + { + "id": "FIX-01", + "title": "首次 GPG 预览与真实安装", + "status": "TARGETED_PASS", + "implementation": [ + { + "path": "manager/backend/services/footstone/domain/repositoryPackageInstallService/service.go", + "sha256": "18eb9a8872e5009bfddfdb42036ef6544f4b86ce8da1a7b76a7bb39c50838619" + }, + { + "path": "scripts/repository-gpg-install-demo.sh", + "sha256": "bb5ed11613011204ddbe8c0030131f06998778b398032420ba8d5a28891915fe" + } + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryPackageInstallService/preview_artifact_linux_test.go", + "scripts/tests/repository_gpg_install_demo_test.sh" + ], + "evidence_summaries": [ + { + "name": "preview-first.log", + "sha256": "06c8d18fc34ca64333144a202c783421dec549ce5d4341a1e6a1ee39530f0742" + }, + { + "name": "bad-key.log", + "sha256": "f09f8ed7776c7fd73a937ea10c910a3dbbdd40bdecf6594d04ef3634211afe01" + }, + { + "name": "confirm.log", + "sha256": "1d6ccc3f13beaaa1187d4965a7ebc1e6a41cf29e31dd0a312376b602ec8d4931" + }, + { + "name": "summary.json", + "sha256": "89c9d6f2d1650530b9c4cdb3ffaaf70db7e2b0fc3a49ee98a3330879cada53d1" + } + ] + }, + { + "id": "FIX-02", + "title": "合法认证反证与直接父任务在途 Retry 去重", + "status": "TARGETED_PASS", + "implementation": [ + { + "path": "manager/backend/services/footstone/domain/repositoryTaskService/service.go", + "sha256": "e23dc1b66ee65b5758e888836365f39c8b309591c0b89efbe6c02c0bf0799ff6" + }, + { + "path": "manager/backend/services/footstone/domain/repository/repositorytaskrepo/repository.go", + "sha256": "8a317b397f6b84fb823e3f6cb8403a0259927b795b818c7488ba6f194c4cad36" + } + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryTaskService/service_integration_test.go", + "manager/backend/services/footstone/domain/repository/repositorytaskrepo/retry_test.go" + ], + "evidence_summaries": [ + { + "name": "mysql-regression-success-retry.log", + "sha256": "2ab39ef9528e0251405220edfbbc4e72479cad3c7428ec256a3180fef0a02113" + } + ] + }, + { + "id": "FIX-03", + "title": "独立租约维护与控制连接恢复", + "status": "TARGETED_PASS", + "implementation": [ + { + "path": "manager/backend/services/footstone/application/repository_maintenance.go", + "sha256": "c5c7e67605b91958e59fc1863b22f6f1c449292f261cedaacf495c1a7743f231" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/remote_worker.go", + "sha256": "b274744356095a7b002adfe1614a761b0d8e1e0aa8e8a21bca436f119d92685f" + } + ], + "tests": [ + "manager/backend/services/footstone/application/repository_maintenance_test.go", + "manager/backend/services/footstone/repositoryworker/remote_worker_recovery_test.go", + "manager/backend/services/footstone/presentation/repository_node_recovery_transport_test.go" + ], + "evidence_summaries": [ + { + "name": "oc9-transport.log", + "sha256": "841ad8d5caa51f47da41cef430931aa3d2da8dc08443a7871408326d52364432" + }, + { + "name": "wsl-transport.log", + "sha256": "83ef5ddbc7659fc2a1e1fcc2f73853ad7d2ea6d9c5b5b0ec5e3d571002890917" + }, + { + "name": "mysql-regression-success-retry.log", + "sha256": "2ab39ef9528e0251405220edfbbc4e72479cad3c7428ec256a3180fef0a02113" + } + ] + }, + { + "id": "FIX-04", + "title": "唯一错误目录补齐", + "status": "TARGETED_PASS", + "implementation": [ + { + "path": "manager/backend/services/footstone/domain/repositoryErrorCatalog/catalog.go", + "sha256": "66c5f558e80eeb4d1fd6343ca4c91a1dec07490c1cd7efe2044db87cf9f02187" + } + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryErrorCatalog/catalog_test.go" + ], + "evidence_summaries": [ + { + "name": "backend-tests.log", + "sha256": "7dc56874df1963b87c2932869a0e6043e5619a1550a849e3cbe3b5e28fa9b490" + } + ] + }, + { + "id": "FIX-05", + "title": "上传对象错误区分与合法 Retry 结果事务", + "status": "TARGETED_PASS", + "implementation": [ + { + "path": "manager/backend/services/footstone/domain/repositoryContentService/remote_upload_retry.go", + "sha256": "5d2883bbc45f03a00696883edf65bc8c72ff1ace99efc3210db8c057164f93ec" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/remote_upload_executor.go", + "sha256": "a144352b58190e856f41199997a1ced8bdf0a447ce26d6388418070f3e57cc2e" + }, + { + "path": "manager/backend/services/footstone/application/repository_node_internal.go", + "sha256": "6ba751cd485876aabd2ae66a20480d179b727822fee8f34198229214eb3d7950" + } + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryContentService/upload_session_test.go", + "manager/backend/services/footstone/domain/repositoryContentService/remote_upload_retry_test.go", + "manager/backend/services/footstone/application/repository_node_failure_integration_test.go" + ], + "evidence_summaries": [ + { + "name": "mysql-regression-success-retry.log", + "sha256": "2ab39ef9528e0251405220edfbbc4e72479cad3c7428ec256a3180fef0a02113" + }, + { + "name": "backend-tests.log", + "sha256": "7dc56874df1963b87c2932869a0e6043e5619a1550a849e3cbe3b5e28fa9b490" + } + ] + }, + { + "id": "FIX-06", + "title": "陈旧节点状态、筛选和完整集合统计", + "status": "TARGETED_PASS", + "implementation": [ + { + "path": "manager/backend/services/footstone/domain/repositoryNodeService/service.go", + "sha256": "e6892fde03c4a15526e79231ca8e5e841798326496ed65fe0773ebf42a9a6d7c" + } + ], + "tests": [ + "manager/backend/services/footstone/domain/repositoryNodeService/liveness_integration_test.go", + "manager/backend/services/footstone/domain/repositoryNodeService/liveness_test.go" + ], + "evidence_summaries": [ + { + "name": "mysql-regression-success-retry.log", + "sha256": "2ab39ef9528e0251405220edfbbc4e72479cad3c7428ec256a3180fef0a02113" + } + ] + }, + { + "id": "FIX-07", + "title": "保留 sysdiagnose HMAC 加固与健康检查", + "status": "TARGETED_PASS", + "implementation": [ + { + "path": "manager/backend/services/sysdiagnose/dependence/tms/gateway_signature.go", + "sha256": "979522d7256055c89915f67f33c3843b5265d3d86f6d877bf5ab239b0f161a4e" + }, + { + "path": "manager/backend/services/sysdiagnose/dependence/tms/tms.go", + "sha256": "90492f6af0305fb989bdceb2aef2e76f25b6a11464ca840410e9bc511fae3875" + }, + { + "path": "manager/deployments/compose/docker-compose.yml", + "sha256": "d0609f879b798bb37d186e523628b3bcb15e51b0d4f55c9764025c7c9feb0a75" + }, + { + "path": "scripts/health-check.sh", + "sha256": "ac7b9ced44363d81aab0869dae4f1555067f4657b4aef231d92a0eae7d8ec30e" + } + ], + "tests": [ + "manager/backend/services/sysdiagnose/dependence/tms/gateway_signature_test.go", + "scripts/tests/health_check_tms_auth_test.sh" + ], + "evidence_summaries": [ + { + "name": "backend-tests.log", + "sha256": "7dc56874df1963b87c2932869a0e6043e5619a1550a849e3cbe3b5e28fa9b490" + }, + { + "name": "shell-contracts-oc9.log", + "sha256": "b7ecf5eb8c1411077b326c3e173977f06c985209749385cf2a2c84518b68e2df" + } + ] + }, + { + "id": "FIX-08", + "title": "命令重跑回填、分页和抽屉可见性", + "status": "TARGETED_PASS", + "implementation": [ + { + "path": "manager/frontend/src/pages/monitor/commandHelper/index.vue", + "sha256": "bdfe64a9f3a7fcdb3bdca28e4f718575cc11f6ca0847bd351aea7391d887aaa6" + }, + { + "path": "manager/frontend/src/pages/monitor/commandHelper/drawerContent.vue", + "sha256": "f69baa900930de2ad96f8bdbabbbb8966006ddb357e5588ab53930860bf79fdf" + }, + { + "path": "manager/frontend/src/pages/monitor/commandHelper/rerun.ts", + "sha256": "384dfe92c9c28a1eec2a09b9967c48032244f3b5ec6da55837f86ac0b9f8cf22" + } + ], + "tests": [ + "manager/frontend/src/pages/monitor/commandHelper/rerun.test.ts", + "manager/frontend/src/pages/monitor/commandHelper/rerun-drawer.test.ts" + ], + "evidence_summaries": [ + { + "name": "operations-final.json", + "sha256": "7d0819f381c6bf35003a9c4ef6c29495439dd899d9f9cf9a96172dcf0e27cad3" + } + ] + }, + { + "id": "FIX-09", + "title": "光盘不进入容量告警但保留原始数据", + "status": "TARGETED_PASS", + "implementation": [ + { + "path": "manager/frontend/src/pages/osportrait/index.vue", + "sha256": "1147c0e24322cd3482fa0b40d11fb62dd7b5b78b8a34e04135cff3c6247a3e20" + }, + { + "path": "manager/frontend/src/pages/osportrait/components/lineChartBox.vue", + "sha256": "8ae3eb1f852abd9a7a5bf6a9b1f979f05a985401f40c9b045eb2e98255b1d89a" + }, + { + "path": "manager/frontend/src/pages/osportrait/utils/disk-capacity.ts", + "sha256": "545f348433fe967a75c05424fadbac38ea299abb611dcef66a00788abab6ec64" + } + ], + "tests": [ + "manager/frontend/src/pages/osportrait/utils/disk-capacity.test.ts", + "manager/frontend/src/pages/osportrait/utils/disk-chart.test.ts" + ], + "evidence_summaries": [ + { + "name": "operations-final.json", + "sha256": "7d0819f381c6bf35003a9c4ef6c29495439dd899d9f9cf9a96172dcf0e27cad3" + } + ] + } + ] +} diff --git a/docs/evidence/repository-style-validation-20261023.json b/docs/evidence/repository-style-validation-20261023.json new file mode 100644 index 0000000000000000000000000000000000000000..cec1a4b55cf9196c196d457e6345ad96928a7abe --- /dev/null +++ b/docs/evidence/repository-style-validation-20261023.json @@ -0,0 +1,2143 @@ +{ + "schema_version": 1, + "date": "2026-10-23", + "scope": "Repository UI consistency and deployed candidate main-flow regression", + "branch": "codex/repository-review-fixes-20261023", + "head": "05312e2483b7827d07a89a6a29c3437c6a7dd42d", + "approved_baseline_unchanged": true, + "committed": false, + "pushed": false, + "pr_updated": false, + "runtime": { + "frontend_image": "sha256:bf6692ad8225d5361611b5eb81ff3976d25dc634b39eaba0747693b6d7d0d115", + "footstone_image": "sha256:f33cf1139ab82925afb820e9bb3f49f53aa8131e827382ae61118a7d5ab970fa", + "frontend_index_sha256": "27f4363992e624db66c0526995e2fc429e8048ce197227a50c8c7a90a819ac0d", + "worker_sha256": "0c7470c6d42a35ae7357d55fac29ee175e3423490eb3324152257d330c230c4a", + "hmac_nonempty_equal": true, + "strict": true, + "old_images_and_worker_backup_retained": true + }, + "quality": { + "repository": { + "files": 17, + "passed": 109 + }, + "operations": { + "passed": 42 + }, + "repository_eslint": "PASS", + "repository_stylelint": "PASS", + "repository_typecheck": "PASS", + "vite_build": "PASS", + "targeted_backend_packages": 7, + "backend_targeted": "PASS" + }, + "browser": { + "widths": [ + 1440, + 1920, + 768 + ], + "desktop_heights": [ + 900, + 1080 + ], + "alignment": [ + { + "route": "list", + "width": 1440, + "pairs": [ + { + "headers": 9, + "cells": 9, + "deltas": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + } + ] + }, + { + "route": "tasks", + "width": 1440, + "pairs": [ + { + "headers": 8, + "cells": 8, + "deltas": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + } + ] + }, + { + "route": "list", + "width": 1920, + "pairs": [ + { + "headers": 9, + "cells": 9, + "deltas": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + } + ] + }, + { + "route": "tasks", + "width": 1920, + "pairs": [ + { + "headers": 8, + "cells": 8, + "deltas": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + } + ] + }, + { + "route": "list", + "width": 768, + "pairs": [ + { + "headers": 9, + "cells": 9, + "deltas": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + } + ] + }, + { + "route": "tasks", + "width": 768, + "pairs": [ + { + "headers": 8, + "cells": 8, + "deltas": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + } + ] + } + ], + "short_status_sample_width_px": 42, + "single_enter_and_clear_requests": true, + "digest_single_line": true, + "whole_page_horizontal_overflow": false, + "ocai_status": "analysis completed" + }, + "product": { + "repository_id": "b52a5690-07d1-44e5-a96c-e8ca6d64dbc1", + "active_revision_id": "026c2fbe-9b5e-46bd-8754-5d768dcc9fa8", + "signed_package_count": 2, + "gpg_policy": "ENFORCE", + "metadata_signing": true, + "tasks": [ + { + "id": "52803ffd-2249-4bf3-ba7d-58b38edaa2d5", + "type": 2, + "status": 5, + "revision_id": "586d09d8-eb5e-4b31-a471-ff20b3bb9025", + "result_items": 0, + "target_statuses": [] + }, + { + "id": "f7661297-93f8-4306-b8c8-21d340d53d02", + "type": 2, + "status": 5, + "revision_id": "026c2fbe-9b5e-46bd-8754-5d768dcc9fa8", + "result_items": 0, + "target_statuses": [] + }, + { + "id": "c9cfc8b8-59a3-4a19-834e-0de0cb0d862a", + "type": 11, + "status": 5, + "revision_id": "026c2fbe-9b5e-46bd-8754-5d768dcc9fa8", + "result_items": 0, + "target_statuses": [ + 5 + ] + }, + { + "id": "5111ade0-0abc-44fa-afd2-1593485c42ab", + "type": 8, + "status": 5, + "revision_id": "026c2fbe-9b5e-46bd-8754-5d768dcc9fa8", + "result_items": 0, + "target_statuses": [] + }, + { + "id": "231bef3e-0de4-47ff-b3ee-357d6a756b8a", + "type": 1, + "status": 5, + "revision_id": "5bb900a0-12ec-4afc-8c96-dea2cdf562b3", + "result_items": 0, + "target_statuses": [] + }, + { + "id": "c785d0eb-9e92-432e-a115-6c94807618e6", + "type": 12, + "status": 5, + "revision_id": "", + "result_items": 0, + "target_statuses": [] + }, + { + "id": "49829438-e21a-445b-8d5f-85091084eb59", + "type": 3, + "status": 5, + "revision_id": "631f2fc9-2831-4ffd-bc26-5a5a26f71794", + "result_items": 2, + "target_statuses": [] + } + ], + "all_listed_tasks_successful": true, + "dnf_install": { + "status": "PASS", + "exit_code": 0, + "rpm_query_exit_code": 0, + "package": "ocm-ui-review-app", + "fresh_installroot": true, + "system_repositories_disabled": true + }, + "negative_cases": { + "missing_gpg_key": "REJECT then original-task Retry SUCCESS after registering key", + "duplicate_content": "NO_PACKAGE_CHANGES; independent upload session", + "wrong_digest": "PREVIEW_CHANGED", + "old_revision_preview": "REVISION_CHANGED", + "inventory_without_key_mapping": "REJECT; explicit mapping SUCCESS" + }, + "log_bundle_members": [ + "context.json", + "result-summary.json", + "task-log.jsonl.zst", + "SHA256SUMS" + ] + }, + "images": [ + { + "path": "docs/images/repository/repository-style-20261023-list.png", + "sha256": "2ae4920b1102fd69f56bc6e676c782c0f05314f0c1d200151f2a2e4c6e285df9", + "original_sha256": "1fa60c29ec69bdffc6eae1918e9c2d30b9eb2957b81a7275ce91c4f463eb19d6", + "address_redacted": true + }, + { + "path": "docs/images/repository/repository-style-20261023-tasks.png", + "sha256": "57bdf5411a58dd25ba5b03cf0fd518d0f06eac78787d158ec0595b2363e54df1" + }, + { + "path": "docs/images/repository/repository-style-20261023-nodes.png", + "sha256": "59b4dc2f4b1d8f2dce8e37294db2e72d418f8ece7a00fd6410db32844c60f662", + "original_sha256": "7eea66875949e76e7b3aca397e67b2687feda4c6ed674aa745a033975dda6b20", + "address_redacted": true + }, + { + "path": "docs/images/repository/repository-style-20261023-sources.png", + "sha256": "d9624d9be072aad14a705a396aae4e69a7d71993feeb57489e38c3072ad2f4b6" + }, + { + "path": "docs/images/repository/repository-style-20261023-node-drawer.png", + "sha256": "f6a051f2ee861f0bc2d645aaa3fe1cb0cadbe8598d9aaee83dd2d9c78f04c4f2", + "original_sha256": "6b032eb100efb22462ed7e06e0a113d9224655e6f5fcd108b93d576a188dd22f", + "address_redacted": true + }, + { + "path": "docs/images/repository/repository-style-20261023-task-drawer.png", + "sha256": "02655fe5ed3da2ce44327582cdd70379285ba65c8e4e3fae3a76f3180370ff34" + }, + { + "path": "docs/images/repository/repository-style-20261023-packages.png", + "sha256": "69e0d967e34117a3bb41b472b8fe5d161d04fcf1ec4629bc7dfca6f57ce57d9f" + }, + { + "path": "docs/images/repository/repository-style-20261023-distribution.png", + "sha256": "3f1fadf0c58264eb7d701d1e946a46777e81fa6cf84601bdf2ab2198c6b0354c", + "original_sha256": "bc00560d155b5fb8bb2f014dab5e479ff50638451b7df8e251a49bddf917370a", + "address_redacted": true + } + ], + "raw_evidence_hashes": [ + { + "name": "product-api-evidence.json", + "sha256": "99dbae99d56be5b7e45d31963d7c13d92cfc5991b4f8918ac008a6f8ce785f27" + }, + { + "name": "terminal-smoke.json", + "sha256": "8624435c64f27754e4a5e3a56ec331063e8ef24a953d8b3072461ec3113be37e" + }, + { + "name": "install-logs.tar", + "sha256": "3d16667eb57d3e8550a961576af8493a2debd4857a1e1ec9c8ff52b7d5e50f97" + }, + { + "name": "task-logs.tar.gz", + "sha256": "e0abbce71e9fb40556727555be0bc82fd380ff293198d3ab22e6d431a63e591e" + }, + { + "name": "deployed-runtime-final.json", + "sha256": "6834f4454fbc1bda7801f6a1a7dd84033c5039850af02a2a2333886166def87e" + }, + { + "name": "source-manifest.json", + "sha256": "6889e6f56ae9add55099a0f45d986cac0232b94c9c7a743cde8a6fc1eb68b311" + }, + { + "name": "alignment-final.json", + "sha256": "853fa89c06f532b73ce950787808c127d5ecd4eaa25a411d322154b9c80b9351" + }, + { + "name": "filter-checks.json", + "sha256": "734534802aa83348537ceb64e1957ce712b01f390774bb3262c44fa5d850d65d" + }, + { + "name": "release-check-final-result.json", + "sha256": "9ee50e2cd55cfb9c7254712649837919a74a8426d3ae06a432ba2145f9d961d5" + }, + { + "name": "frontend-tests-last.json", + "sha256": "63e72ce4e2a555539c74e62235f579ba1c2e973165c54b8aa44979f88aa06370" + } + ], + "source_references": [ + { + "path": "manager/backend/services/footstone/Makefile", + "sha256": "a0e016024711b2ddfc188f6b2985da085fca3b7921f0820183a1aadc78b63b5a" + }, + { + "path": "manager/backend/services/footstone/README.md", + "sha256": "d21237ba51ce4ad0631315084892aaa8a4393489707ebc469eab14a1c620613f" + }, + { + "path": "manager/backend/services/footstone/VERSION", + "sha256": "4ca8bbd3c595ee306226c6996b32b8d47a900217c55a7ed0cfc868ee444f34f1" + }, + { + "path": "manager/backend/services/footstone/application/access.go", + "sha256": "7e678976b4ad9078ee1b2e27b0134fa622689b39265984554ffe5b4d83865abf" + }, + { + "path": "manager/backend/services/footstone/application/application.go", + "sha256": "b56f57cee2a7377fbcb9af6fceff4fa16f6c68377f1144ccb3626b75fd83de4a" + }, + { + "path": "manager/backend/services/footstone/application/application_cache_test.go", + "sha256": "bf7f7e54b7a33dac55035b15e1cb56bd8eadded68f2d5e85dff6d4768d871518" + }, + { + "path": "manager/backend/services/footstone/application/application_test.go", + "sha256": "f6866004f46ca87df977b3b30e2f171bc5ed83b47b7272de32f448521ac66da6" + }, + { + "path": "manager/backend/services/footstone/application/audit.go", + "sha256": "524240e370535d7cdbecf31b210ef13098b2c64bc87ef7fc02dafeb0b4cf624b" + }, + { + "path": "manager/backend/services/footstone/application/cluster.go", + "sha256": "37aee51787a7d498e829321e40bb2f2b143828257fa1da36f3197e8d7d5ceb8f" + }, + { + "path": "manager/backend/services/footstone/application/hierarchy.go", + "sha256": "e8de5ed6f7bd09560e693043195a8b1413d75978f031bc84510fde1635e7f30e" + }, + { + "path": "manager/backend/services/footstone/application/instance.go", + "sha256": "475e6a81bdc4aaac9b3185be0944c6fb79c10df39d653b5f6ee212a232119142" + }, + { + "path": "manager/backend/services/footstone/application/instance_cache_test.go", + "sha256": "f04495be529797dd147793bcd987026937d0dbce8e8f31981d0678bc7157e28e" + }, + { + "path": "manager/backend/services/footstone/application/instance_lock.go", + "sha256": "7a174ee393cf2b7c169ae05b2b09380fb0799a04725c9555a167a30e7bc9d042" + }, + { + "path": "manager/backend/services/footstone/application/instance_lock_test.go", + "sha256": "53ce807b3a3ccbb5fb630643dbe9488206971ccbe219dc3816eb3fa2e0d8ae69" + }, + { + "path": "manager/backend/services/footstone/application/instance_mapping_key_test.go", + "sha256": "c14a3e1d1f8739786ae0ce2647845bcc48c55d122725efbbdccafca2cbb69787" + }, + { + "path": "manager/backend/services/footstone/application/metrics_meta.go", + "sha256": "fc1c65ed5815e4b8ad868521cae9e4662ba91e3a76447074b8e638acc3e7182f" + }, + { + "path": "manager/backend/services/footstone/application/monitor_sweep.go", + "sha256": "87c072479db864f0f6743fa57d0f06749ac5cec8e45b6738a5640ddc50c88755" + }, + { + "path": "manager/backend/services/footstone/application/repository.go", + "sha256": "4bcde49ad5213eb43c312addd99cb53aaf21e8dbd5354d0f910fb132c181b63a" + }, + { + "path": "manager/backend/services/footstone/application/repository_content.go", + "sha256": "c12b3e14caa2cfbe35debd682586a048c040a0ab8511436a2cc727e30f273970" + }, + { + "path": "manager/backend/services/footstone/application/repository_distribution.go", + "sha256": "bda721985daa9e32ce54e811f264641eb3460a18d315ee4e19e50cb832a82d0b" + }, + { + "path": "manager/backend/services/footstone/application/repository_maintenance.go", + "sha256": "c5c7e67605b91958e59fc1863b22f6f1c449292f261cedaacf495c1a7743f231" + }, + { + "path": "manager/backend/services/footstone/application/repository_maintenance_test.go", + "sha256": "40742e19366d3f36158c30a91dfa00ffd676f188f8a47fd6bcddfdb591739d3e" + }, + { + "path": "manager/backend/services/footstone/application/repository_node.go", + "sha256": "5a1c5068c247de12fe2d4555037165109be470dc1c6b67a93a08acdd2ea0aa6a" + }, + { + "path": "manager/backend/services/footstone/application/repository_node_failure_integration_test.go", + "sha256": "401c4f63544e2aa89e594a1d248c211acd26ed75b517a17ff419e381d7bbf419" + }, + { + "path": "manager/backend/services/footstone/application/repository_node_gpg_integration_test.go", + "sha256": "a6fa87f8564ff5aa7fc26400c18ebc4635bda9cf4f0d6bf88df79c6a0f766f7c" + }, + { + "path": "manager/backend/services/footstone/application/repository_node_install.go", + "sha256": "2e4fff927cccd89dd4dfcf0086cae84f630cbdedc22669e8041350570982513b" + }, + { + "path": "manager/backend/services/footstone/application/repository_node_install_test.go", + "sha256": "9f25eb59f8c5cbfaf829adc6766bdfe920dfe4a99b526ef66c8a839e140f96e8" + }, + { + "path": "manager/backend/services/footstone/application/repository_node_internal.go", + "sha256": "6ba751cd485876aabd2ae66a20480d179b727822fee8f34198229214eb3d7950" + }, + { + "path": "manager/backend/services/footstone/application/repository_node_upload_adapter.go", + "sha256": "cbed0314f54cdc686dbebe8706740983f671ad5fef9fbc31e1bc405aca95b57c" + }, + { + "path": "manager/backend/services/footstone/application/repository_ocai.go", + "sha256": "056b121350a5e7c0090ceeb05b211ba99369c892bc90e8951ebda5ca4abad1da" + }, + { + "path": "manager/backend/services/footstone/application/repository_ocai_test.go", + "sha256": "4298c1531f6319a61479217f1762598a7bb3ca65e386cf95fe7eb396ecc3bdd5" + }, + { + "path": "manager/backend/services/footstone/application/repository_package_install.go", + "sha256": "ce54ba69d1a38374d22baf7ed6c0a1d70f6bdfba158ade8d3119de8ca78cf471" + }, + { + "path": "manager/backend/services/footstone/application/repository_report.go", + "sha256": "313b0c72a34082723c517ef25551072e7dac610bc3db71971d0f948c57b1b214" + }, + { + "path": "manager/backend/services/footstone/application/repository_schedule.go", + "sha256": "f6fa02663ad9bb8b2eabc9304d1dbb080d51033c4bd085255fcdc4250b0cc9e4" + }, + { + "path": "manager/backend/services/footstone/application/repository_task.go", + "sha256": "a21f7014e48957de927fd12c1f74d17af26107e66977358014890c1cf5dd2142" + }, + { + "path": "manager/backend/services/footstone/application/repository_task_log.go", + "sha256": "3a9f02498f06925e59727d3fa9aac640f45a141a6e3dcb3b4cf40381931bbb19" + }, + { + "path": "manager/backend/services/footstone/application/repository_task_log_test.go", + "sha256": "91c0e070139cc8f499e5c78727b0bda8e6da06685c1c484d7fcef0df923130ea" + }, + { + "path": "manager/backend/services/footstone/application/repository_task_test.go", + "sha256": "9bb13feec864bf1bed79614f982313840bfbe0f5958b4ff4209241034af01d59" + }, + { + "path": "manager/backend/services/footstone/application/repository_test.go", + "sha256": "e6061866578618ea061caf8f5b1f2887904d424033b8e547cb6ff68d8fb37a96" + }, + { + "path": "manager/backend/services/footstone/application/repository_upstream.go", + "sha256": "67aecb7f5435041f4eba4d4f3540e806f8ca4a852760fd5dce10f5f94278a31d" + }, + { + "path": "manager/backend/services/footstone/application/user.go", + "sha256": "5ca3af908fc0e7137747a153c5808faca8a7317c3ceac868379359277b313608" + }, + { + "path": "manager/backend/services/footstone/build/Dockerfile", + "sha256": "0207d47242d619c11c5884ffd559d613b3dba81fa59128b4bea6ad9e67c4709b" + }, + { + "path": "manager/backend/services/footstone/build/build.sh", + "sha256": "0044b9b6e9bc0c6aeb7eb127edf24bde706af189691969c8057ae68e368f1963" + }, + { + "path": "manager/backend/services/footstone/build/build_static.sh", + "sha256": "69331bfc8e76332ca6fc90e1a1f230cc9a4f7a8a8a240de8926e3427a741c2aa" + }, + { + "path": "manager/backend/services/footstone/build/entrypoint.sh", + "sha256": "670919e87d910dd99ac050cfc85cbe0a990a9f94cadb0d05d7cb93f38dfb2ff2" + }, + { + "path": "manager/backend/services/footstone/build/entrypoint_gateway.sh", + "sha256": "509ba06652ae03a3db3758e92f5c2321563156938f022071d024350d1c34f262" + }, + { + "path": "manager/backend/services/footstone/build/entrypoint_stream.sh", + "sha256": "cf49b82eda8f61651e63bb1855db4716b6a5e83b20d7888e6c8a8302b8edf43c" + }, + { + "path": "manager/backend/services/footstone/build/start.sh", + "sha256": "95821ed1a2944824ec7d546c94be44d918939eb1610dca26fcfbe3ef66ddf227" + }, + { + "path": "manager/backend/services/footstone/client/footstone_test.go", + "sha256": "1df2600d23dfdde9db170357f0a6bd32cf8a6aa6d8f0f894079c94cb4545ac53" + }, + { + "path": "manager/backend/services/footstone/configs/application.yaml.template", + "sha256": "1cdb0bfedb17a6d363bef4f4653061bfb83ade87fdbed2065b9438a68144981d" + }, + { + "path": "manager/backend/services/footstone/configs/trpc_go.yaml.template", + "sha256": "a99b75de66470b7118fbebe31acff5a3902b1241f68101bc29899de5b3c151a2" + }, + { + "path": "manager/backend/services/footstone/dependence/aggregation.go", + "sha256": "700902f681c358b3b035d0a46aae723dcc4c0da99167fbf606ccc549e8653e83" + }, + { + "path": "manager/backend/services/footstone/dependence/metrics/metrics.go", + "sha256": "268be8228ac2479cbfbf46a63e8e34a0762c88b2d3e4316b74650566df4b8c39" + }, + { + "path": "manager/backend/services/footstone/deployments/docker/Dockerfile", + "sha256": "683386162a4ec18628f0bf3db2b9e69bdb65ae9705a17557c841c59b3759e851" + }, + { + "path": "manager/backend/services/footstone/domain/accessService/service.go", + "sha256": "d504d361e42d70b755a1aa84cb2856901b878dcdbec8853bc9fb14d20671e0fc" + }, + { + "path": "manager/backend/services/footstone/domain/accessService/service_test.go", + "sha256": "fc8259a5da2a4df081c7edec61037fef3288b89c1ac3e7e798118170db4e0fc6" + }, + { + "path": "manager/backend/services/footstone/domain/auditService/audit.go", + "sha256": "43641e53eb0818db480ac246510f56a4715789d8464fffe0a615403eec440f80" + }, + { + "path": "manager/backend/services/footstone/domain/auditService/audit_op_test.go", + "sha256": "5402091dd207f8735145cc84dffdd713044288ea2733823711cf98f26a831a6a" + }, + { + "path": "manager/backend/services/footstone/domain/auditService/audit_test.go", + "sha256": "f0fd3aa25f4225db64271a89628df4a00f172cb7ff97a3e9117a9f54e3165727" + }, + { + "path": "manager/backend/services/footstone/domain/cacheService/service.go", + "sha256": "78a0881ac2069d37d7be36f2164ad08c1a5230fb688ea658e43de90561d6d75b" + }, + { + "path": "manager/backend/services/footstone/domain/entity/constant.go", + "sha256": "d71618dd0a231e3509c1b946ccbd9b67e8b19ab03d74c04433eb2689c592c467" + }, + { + "path": "manager/backend/services/footstone/domain/entity/context.go", + "sha256": "1f66271261de296cd813a3a976e18c467730192bd6d014c19d888fca62c90363" + }, + { + "path": "manager/backend/services/footstone/domain/entity/request/access.go", + "sha256": "e301d9c0bb2d6d0cc30bcc352e68964989fe5d4a2ac98cdf0ba2d9d3741dabe2" + }, + { + "path": "manager/backend/services/footstone/domain/entity/request/audit.go", + "sha256": "7df267439146c984235dcc5aa4688b1d295df6a67b58315bc08c8ecb51d789db" + }, + { + "path": "manager/backend/services/footstone/domain/entity/request/cluster.go", + "sha256": "947d6bd6c16cceec1b3c9143cab51933d7ecbe09414b1d61e576017911c6d723" + }, + { + "path": "manager/backend/services/footstone/domain/entity/request/hierarchy.go", + "sha256": "d43e01d700b6f30b1635e12a0ecf676698110a50944728c42ddcad2ae29148d5" + }, + { + "path": "manager/backend/services/footstone/domain/entity/request/instance.go", + "sha256": "82dd4d18fd0993f559b370881efe7cd5bfa9fa31437abad7e0d3d2bc8a39e23d" + }, + { + "path": "manager/backend/services/footstone/domain/entity/request/paging.go", + "sha256": "364000387483d2bb22ba4dd241b790fd2085c8a833d51992b6f772a9bdb1352a" + }, + { + "path": "manager/backend/services/footstone/domain/entity/request/paging_test.go", + "sha256": "2f0e097264c7241af87d8b82eae52ee1d4b5078735faedf7765004b5c1ed69ec" + }, + { + "path": "manager/backend/services/footstone/domain/entity/request/project.go", + "sha256": "2a5e49c92927d10cf9352ab1b17c0b9b153cf50bf324c29c4b504911995836e1" + }, + { + "path": "manager/backend/services/footstone/domain/entity/request/repository.go", + "sha256": "f97f4f255445d71ad85d836024cbb490416332a307b8d1832f2bd74fb6ace65d" + }, + { + "path": "manager/backend/services/footstone/domain/entity/request/repository_content.go", + "sha256": "08c4a14f071f29c54352a7e2325259055e50e63be155c9f7848d333efe8eb99f" + }, + { + "path": "manager/backend/services/footstone/domain/entity/request/repository_distribution.go", + "sha256": "d6c031153dbfc0af1682a728c6bd8bad2fa797c6ab6473b21fb58bf7607d3d1e" + }, + { + "path": "manager/backend/services/footstone/domain/entity/request/repository_inventory.go", + "sha256": "ab01ec7cf10cfe18f8a9c10865991b240c1475df95ca72635fd7c9ac25ca070b" + }, + { + "path": "manager/backend/services/footstone/domain/entity/request/repository_node.go", + "sha256": "d6751b4aa30a1eda7dfc35b9d79111fb22ec3956b13c57ff97ce7f2aa4b6db30" + }, + { + "path": "manager/backend/services/footstone/domain/entity/request/repository_ocai.go", + "sha256": "34eb94e610710f2383839eac365f6cb0ce3f4c5fcce2f8280138fdd710089ac3" + }, + { + "path": "manager/backend/services/footstone/domain/entity/request/repository_package_install.go", + "sha256": "9613e0b0b287334d8cb920b6c2577622e5aa88f245a30061f07fdfe9a315ae7d" + }, + { + "path": "manager/backend/services/footstone/domain/entity/request/repository_report.go", + "sha256": "b02857334c95035534644cd0d4f05b3ae6ae015a17d461459315977f0d013747" + }, + { + "path": "manager/backend/services/footstone/domain/entity/request/repository_schedule.go", + "sha256": "b476a5029f4204f8f181ef1172565419872984a236566f07d88286b94af2bff8" + }, + { + "path": "manager/backend/services/footstone/domain/entity/request/repository_task.go", + "sha256": "86bd79d09c19559d7f35a26f847d9dd29a9f879c3639db0c455e4f45b6701a88" + }, + { + "path": "manager/backend/services/footstone/domain/entity/request/repository_upstream.go", + "sha256": "b17ecd988621f7cca929ba20f644907efc658a368de713eee982f7a2fe08c531" + }, + { + "path": "manager/backend/services/footstone/domain/entity/request/user.go", + "sha256": "35a245924e7fec2e2cc53cc7c5ad0e5b825455e169e76f1b0fd82f73ebfd667e" + }, + { + "path": "manager/backend/services/footstone/domain/entity/session.go", + "sha256": "bb097e06d6f206fbc4dbfd5bf4087eb83200b891e5942b52cd0c46f6e9404f7b" + }, + { + "path": "manager/backend/services/footstone/domain/entity/session_test.go", + "sha256": "09e13ee3e13a93f7085561b6bf084b3c719d6532581916644824226ad067af17" + }, + { + "path": "manager/backend/services/footstone/domain/footstoneService/cluster.go", + "sha256": "e96443e7187213e13bc9c5e7ecb268daa95ac9c6b97a319891012104cbba5098" + }, + { + "path": "manager/backend/services/footstone/domain/footstoneService/cluster_test.go", + "sha256": "f0b6ee9dd3a0072f34c225c73ba6e313838ac4ee790da95c76cc1ad201576c3e" + }, + { + "path": "manager/backend/services/footstone/domain/footstoneService/clusterconfig/cluster.go", + "sha256": "a1b8b3870a7c71c5d87bb2099b6af4f5a0454285fdb91ce9d2f2f4a1379b385d" + }, + { + "path": "manager/backend/services/footstone/domain/footstoneService/clusterconfig/cluster_test.go", + "sha256": "4ee2177a88571829b3e1fc48779d598946326c9ec9aac4b4858ef9e5d3f58e5e" + }, + { + "path": "manager/backend/services/footstone/domain/footstoneService/clusterconfig/proeject_test.go", + "sha256": "149f2893ba7a27f77f8258b15bad6f0c7c7655895e213934bae6e6904a354817" + }, + { + "path": "manager/backend/services/footstone/domain/footstoneService/error.go", + "sha256": "cc135e74851647fb6e8f851981db30b81dc28eca2bf97a4c868a739988ab3255" + }, + { + "path": "manager/backend/services/footstone/domain/footstoneService/hierarchy.go", + "sha256": "64e2cbb0ab35344069c16dc1164bd11ffbc6ac38ba90eb61aff602be2e1265d5" + }, + { + "path": "manager/backend/services/footstone/domain/footstoneService/instance.go", + "sha256": "74e844c6fedf006a5f599b246efc5f2bce137b8086e5aa1a27c3bff83e48e40d" + }, + { + "path": "manager/backend/services/footstone/domain/footstoneService/instance_error_test.go", + "sha256": "038331ca14ae3a6da06f3eb283a1b43ac2b6af77ea5157a8bcfd813407355349" + }, + { + "path": "manager/backend/services/footstone/domain/footstoneService/instance_export_test.go", + "sha256": "90831168f5e61b834c1f334eb3ed35e7d27fd190ace2fce8c172f306ce0fa1fc" + }, + { + "path": "manager/backend/services/footstone/domain/footstoneService/instance_kernel_modules_test.go", + "sha256": "a22fabc41226e1bd2f34a14b03163e6611c0b7c33d0a0197c03443a9c556ac39" + }, + { + "path": "manager/backend/services/footstone/domain/footstoneService/instance_test.go", + "sha256": "69e7b424515115beda2d3b99c3cd8a856525de223c10173cd662c18d19142947" + }, + { + "path": "manager/backend/services/footstone/domain/footstoneService/metrics_meta.go", + "sha256": "ed4e04e37964aeec25fc855d83a99bcfca103b9b03e3ae2633fb591a34c53fd5" + }, + { + "path": "manager/backend/services/footstone/domain/footstoneService/metrics_proxy_test.go", + "sha256": "f0f2bfebc33621abfd992ec331d194995af93cac16ef0d8e3941b1f6adcd351e" + }, + { + "path": "manager/backend/services/footstone/domain/footstoneService/monitor_sweep.go", + "sha256": "bcc147aae88e4cc0e62583ce87229946f1ff43d465d36554c6924c68f147bc40" + }, + { + "path": "manager/backend/services/footstone/domain/footstoneService/monitor_sweep_test.go", + "sha256": "0b5df6d27d5f1bbee184ed2e6a8a1816bb872b5b96e6ad8dd383dcfbf5ec5424" + }, + { + "path": "manager/backend/services/footstone/domain/footstoneService/project.go", + "sha256": "34d8e0f9bdf71f714cb6bec92d22036e05d74907ae8d4c3a791aabfabf72d5fa" + }, + { + "path": "manager/backend/services/footstone/domain/footstoneService/project_delete_error_test.go", + "sha256": "2863cd823946c0b7492b30afff35111e901493f74260ac51398107ca80fb5755" + }, + { + "path": "manager/backend/services/footstone/domain/footstoneService/project_pagination_test.go", + "sha256": "4fee83211f748e4cb58d80a0a9f8361abebec36fd50f0bbf1662657782602d13" + }, + { + "path": "manager/backend/services/footstone/domain/footstoneService/project_sort_test.go", + "sha256": "8d0e8afb20b6540ffc8662840816eb18ef63f126731b4f7b3d8e3f4f6c6eef26" + }, + { + "path": "manager/backend/services/footstone/domain/footstoneService/project_test.go", + "sha256": "f89194e3d06730b7f2b752eeffbc6d0ea702f395e59f3aadd1e5c3d400bc1500" + }, + { + "path": "manager/backend/services/footstone/domain/footstoneService/quick_search_test.go", + "sha256": "a81825a336fe2e80536a3475c7a434777d1c894e6896d3e8ce071424884016a2" + }, + { + "path": "manager/backend/services/footstone/domain/footstoneService/service.go", + "sha256": "71d09a2cc2f1f7ecfade0a69aee6d28d6973a640dd295f4f4f3bca4dcd8bedde" + }, + { + "path": "manager/backend/services/footstone/domain/footstoneService/user_favorite.go", + "sha256": "f28936480588ca8296fc9c7b0c0f62234e45d7db390e7e12ed459daafaa14fbc" + }, + { + "path": "manager/backend/services/footstone/domain/repository/baserepo/dbrepo.go", + "sha256": "8f54d18ace77b1895242f7213944518da55190ea521633135e5df6e4664e2ed2" + }, + { + "path": "manager/backend/services/footstone/domain/repository/favoriterepo/user_favorite.go", + "sha256": "9506a78e7ffe88598fd4a92ca8cb74507bd512f5e562f46605286fc8f0d4e741" + }, + { + "path": "manager/backend/services/footstone/domain/repository/favoriterepo/user_favorite_test.go", + "sha256": "4b2741895a82f9aa2a12843f6c2cf1ec51f7b64beeb1f2a8a78d3e7691631f37" + }, + { + "path": "manager/backend/services/footstone/domain/repository/infrarepo/audit.go", + "sha256": "e9cc1600dd32642453e2b75a27bdcec60a7ce8ff1bd19fa756f14f411c9a0f15" + }, + { + "path": "manager/backend/services/footstone/domain/repository/infrarepo/project.go", + "sha256": "bcc8785672f41e6710a459aa4f191a6b2f43f6cf4da3c95e0b9e200619e9a8c2" + }, + { + "path": "manager/backend/services/footstone/domain/repository/infrarepo/project_test.go", + "sha256": "10125df35067392dd122860967bf7fa1d1f554d1cdb5385dc699ffa699921d13" + }, + { + "path": "manager/backend/services/footstone/domain/repository/infrarepo/quick_search_test.go", + "sha256": "85190050149431dd485949e6f49dbe9d800a0ab90dfbaaa372753962dea45056" + }, + { + "path": "manager/backend/services/footstone/domain/repository/infrarepo/repo.go", + "sha256": "b08b18af36cfce5f2e64c839486c027ae3aeffb6fab513ad394e1d33c9e29afd" + }, + { + "path": "manager/backend/services/footstone/domain/repository/instancerepo/base_repo.go", + "sha256": "43ad119273e10a75ef54b47e39a8cd85d00c862536f5da8f17bc90a31919cff2" + }, + { + "path": "manager/backend/services/footstone/domain/repository/instancerepo/instance_ip_mapping_test.go", + "sha256": "46f8d9d0e94d3c4047cf8c13eb314512425f711db3d20f3649351ec318d79f54" + }, + { + "path": "manager/backend/services/footstone/domain/repository/instancerepo/ip_filter_test.go", + "sha256": "3e6a3ce3c06c2571a93d3d30ad2a7a760797bd1465f6622ef4aa04ba9190424d" + }, + { + "path": "manager/backend/services/footstone/domain/repository/instancerepo/list_instances_order_test.go", + "sha256": "d79d68cd90d5742f68c11fb87d040c4a1c0d1c01600845620100b504d5fc990f" + }, + { + "path": "manager/backend/services/footstone/domain/repository/instancerepo/list_os_names_test.go", + "sha256": "0ce2f9779b74f4a7d1b5cabc24aaed5a8c004d17c2d4c5a7c7d4bde5eb6e6ad4" + }, + { + "path": "manager/backend/services/footstone/domain/repository/instancerepo/matched_ips_test.go", + "sha256": "a5b1436ef1e57dad8e13aaa8bcf7b450c56cd609e8e27cbd7bb4b342c0fd98c2" + }, + { + "path": "manager/backend/services/footstone/domain/repository/instancerepo/monitor_sweep.go", + "sha256": "6b7566a734991d5b54f628288410f2aede07de5a8b68d9c60b83f0945eae60a0" + }, + { + "path": "manager/backend/services/footstone/domain/repository/instancerepo/repo.go", + "sha256": "984341e2ec92f5e53ea625880f48e91db3261da7241a1cc957ccff73cb20a1ca" + }, + { + "path": "manager/backend/services/footstone/domain/repository/instancerepo/repo_test.go", + "sha256": "336d90b04d5a60c06f35ee089a3c9e674f31d28ef95951b77e65fb427cca1044" + }, + { + "path": "manager/backend/services/footstone/domain/repository/metricsmetarepo/repo.go", + "sha256": "06dfa404a0119678cd42fd1beb703c062cb435fc3c130a49dd207da533bcbeb5" + }, + { + "path": "manager/backend/services/footstone/domain/repository/metricsrepo/os_monitor.go", + "sha256": "312ec1df4b18d1f68d9a4e4598bc51805c66fc3fb2155b474d1df2545e734475" + }, + { + "path": "manager/backend/services/footstone/domain/repository/metricsrepo/repo.go", + "sha256": "71d4d7a38b698259504505779d1e818dd150fffd3d14de4f3c93e6d787713b2d" + }, + { + "path": "manager/backend/services/footstone/domain/repository/po/audit.go", + "sha256": "260b6383c711480faf83b509cb93bf3fe7541061766808203e34f4fad01d36a4" + }, + { + "path": "manager/backend/services/footstone/domain/repository/po/audit_test.go", + "sha256": "de63948bbc6d0e87f2508732aed61d993bb71d6d05919477de9b2d91c313c308" + }, + { + "path": "manager/backend/services/footstone/domain/repository/po/favorite.go", + "sha256": "d6d5f9bf702819a13f1df2933c343c6371374ae784c5657379669b5136c90552" + }, + { + "path": "manager/backend/services/footstone/domain/repository/po/infra.go", + "sha256": "ff2c700c6f5fb6c56d65abde437d7002a78198ffcf63a6b3fc863805b4b83fca" + }, + { + "path": "manager/backend/services/footstone/domain/repository/po/instance.go", + "sha256": "8fb2593d50927f554b8cc00c4529a9e721012a49e85ca660dc53b7af2a56cf77" + }, + { + "path": "manager/backend/services/footstone/domain/repository/po/metrics_meta.go", + "sha256": "38ab234e201b1a8136ebaaeb5a3a685571c23cac518f1d652bbda69428883874" + }, + { + "path": "manager/backend/services/footstone/domain/repository/po/repository_acceptance_closure.go", + "sha256": "a2c0195e13382717e79785c48bc91028e921b858bd3d51579af76a191dd1beee" + }, + { + "path": "manager/backend/services/footstone/domain/repository/po/repository_content.go", + "sha256": "68e8856b4926cc566d91a20ec8efdbb6e5427cd415ec90b5a86d0ae021eb5ab0" + }, + { + "path": "manager/backend/services/footstone/domain/repository/po/repository_distribution.go", + "sha256": "c848f1b91f2e5ed81138a64cb9321f52917eb5e61acd8f8786ec5aef93101b50" + }, + { + "path": "manager/backend/services/footstone/domain/repository/po/repository_inventory.go", + "sha256": "9251e3ccd17880d822f684625b6151db712a265fc66a299ce3cf9728517abba5" + }, + { + "path": "manager/backend/services/footstone/domain/repository/po/repository_node.go", + "sha256": "affade4486223b0c42ec7db04d8b174f11361a4f4492278f186cb653660fd228" + }, + { + "path": "manager/backend/services/footstone/domain/repository/po/repository_node_lifecycle.go", + "sha256": "12f28f70fd5bac6b54aec19082221623df4db93b14394a81397237c7dcb02b7d" + }, + { + "path": "manager/backend/services/footstone/domain/repository/po/repository_package_install.go", + "sha256": "47bc1f9f003da963c04b25f534620e27f8a03e7cca65644646056f320455e7c5" + }, + { + "path": "manager/backend/services/footstone/domain/repository/po/repository_schedule.go", + "sha256": "bb1e1409bbfa8288f773aafdc0dec0f32c6e95164843067025bb5db1834b503b" + }, + { + "path": "manager/backend/services/footstone/domain/repository/po/repository_task.go", + "sha256": "2b8814b3c7cdb3559d3103cd350ba14b02df85d1cd151af8bef4a6da0f492fd0" + }, + { + "path": "manager/backend/services/footstone/domain/repository/po/repository_upstream_profile.go", + "sha256": "7cc6fea17c0a3a3f48b3275033fe2ccb3a94df3659a068da570027ed6184691e" + }, + { + "path": "manager/backend/services/footstone/domain/repository/po/repository_upstream_profile_test.go", + "sha256": "db55c7f4bf224ab84b0638f5e3804fd11884bf0dea32c778fd7a77d77fef9db6" + }, + { + "path": "manager/backend/services/footstone/domain/repository/po/software_repository.go", + "sha256": "f6581c7d5a2ea8554738f67d315021eb9e101df11c63864604ea7563e820d5f6" + }, + { + "path": "manager/backend/services/footstone/domain/repository/po/software_repository_test.go", + "sha256": "6a9752a276ab1904a7ed08791d30595067bcaf7191e1e39ef91413713f3dab22" + }, + { + "path": "manager/backend/services/footstone/domain/repository/po/user.go", + "sha256": "dffc74ddf1f1db6dfc3c5ddcfa76de989221ca245164bbab242cc3f18a17f65b" + }, + { + "path": "manager/backend/services/footstone/domain/repository/repositoryairepo/repository.go", + "sha256": "b3c9785cdbc4b1005176e3742f9b7d7a24c702a7925b2cef9adcbf7cd8c9d528" + }, + { + "path": "manager/backend/services/footstone/domain/repository/repositorycontentrepo/repository.go", + "sha256": "fcee5f344d9dd7f14b35674fd68ba921f4c4bc1e12f256a5c10e1fb3aebbd721" + }, + { + "path": "manager/backend/services/footstone/domain/repository/repositorydistributionrepo/repository.go", + "sha256": "ed125a4da5a82ea553239a454617410d4cbe85e09212653c91486257f037365e" + }, + { + "path": "manager/backend/services/footstone/domain/repository/repositorydistributionrepo/repository_test.go", + "sha256": "fab8eded7f3c7ca2c8cf7c09daa0a183bd83c45d34afedebc282b3b04a4229f2" + }, + { + "path": "manager/backend/services/footstone/domain/repository/repositoryinventoryrepo/repository.go", + "sha256": "e49f2fa056d05d73e4d198755e697b02c0e72087c13835b0b3171dc188fe311d" + }, + { + "path": "manager/backend/services/footstone/domain/repository/repositorylockrepo/repository.go", + "sha256": "76ec740718eb3b81e3514fbbd3dfa50d0678c34b47396f73ce2be3b346377c94" + }, + { + "path": "manager/backend/services/footstone/domain/repository/repositorypackageinstallrepo/repository.go", + "sha256": "d57410ac83463cd800afdbe152ea806ab8ac10d3825e5ee5fa93a70f5ddcda8c" + }, + { + "path": "manager/backend/services/footstone/domain/repository/repositoryrepo/repository.go", + "sha256": "c6720c9d46c8fe221f7beb720f36910882c5f6c7adf091881c5881b73f085d34" + }, + { + "path": "manager/backend/services/footstone/domain/repository/repositoryrepo/repository_test.go", + "sha256": "ae18464d01a3039332aa52df0eab2f72d2e40f881898bb5df42d79115b8e3156" + }, + { + "path": "manager/backend/services/footstone/domain/repository/repositoryschedulerepo/repository.go", + "sha256": "ad308c31fcaf843ed52e4285e678b9ad711909d5c6463bd0dc2a704d53b9ea01" + }, + { + "path": "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/lifecycle.go", + "sha256": "116af4212658e56222501eeeb4855721c0fb935ab2e049d99821c0652873a28a" + }, + { + "path": "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/lifecycle_mysql_integration_test.go", + "sha256": "aeb9d0726844284d49d2dd099d7fd3353472edc39a36fa681c8726d9ad11dba0" + }, + { + "path": "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/lifecycle_test.go", + "sha256": "a8bde6d482c680c77312a4e16e4feaae5f80daa87b77f26e07bf7cbea1c462be" + }, + { + "path": "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/repository.go", + "sha256": "0824451fe4c6f0556aeeb0b94d9e999749ccd2e582865cd47a55a58fb6e36daa" + }, + { + "path": "manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/statistics_test.go", + "sha256": "ca69d3f5a718848fe1168340ef967b199244156f0707118fbf762a8c62ec90e8" + }, + { + "path": "manager/backend/services/footstone/domain/repository/repositorytaskrepo/repository.go", + "sha256": "8a317b397f6b84fb823e3f6cb8403a0259927b795b818c7488ba6f194c4cad36" + }, + { + "path": "manager/backend/services/footstone/domain/repository/repositorytaskrepo/retry_test.go", + "sha256": "db9311b09f6d770f59f3c7460d3aab2ea44f5018748918b76e9d3ea8013f607c" + }, + { + "path": "manager/backend/services/footstone/domain/repository/repositorytaskrepo/statistics_test.go", + "sha256": "8e39fd5ab85880bc12a7a25f71b5e76eeb30a966308d7c2b1efc1b4cad8470b9" + }, + { + "path": "manager/backend/services/footstone/domain/repository/repositorytaskresultrepo/repository.go", + "sha256": "7c5eeed03858064ca0288439b4f9fe3307da62a59d7918f398bcc80131882f5c" + }, + { + "path": "manager/backend/services/footstone/domain/repository/repositoryupstreamprofilerepo/repository.go", + "sha256": "896fb932f73ae4358249f9c54dae04e6b20d55262524e07bf3c6edfb2caff3b9" + }, + { + "path": "manager/backend/services/footstone/domain/repository/userrepo/modify_user_test.go", + "sha256": "9785866e6371f30c08abd21fbe6ab7abf368fceb8585e5a878168dc39c75985f" + }, + { + "path": "manager/backend/services/footstone/domain/repository/userrepo/repo.go", + "sha256": "d84506c51d27abfbc9cf6dba7759b17aaadf24254170833488fe46dbb5abc159" + }, + { + "path": "manager/backend/services/footstone/domain/repository/userrepo/repo_test.go", + "sha256": "528a7bb75ad959eb2e20f3cbcc68bf606a725dfd69978211ff70cdd25c7744c8" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryAIService/service.go", + "sha256": "f37447c83ccf9810930609fed23b70c851d0b31dbfb4b8563476754a7a3875e5" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryAIService/service_test.go", + "sha256": "f49f7f9efce3462f96af6d821ba4c63f03e2e061b92d73a19d932bf6578b2fed" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryContentService/gpg_key.go", + "sha256": "4c5da4df09ffddb632a66211f0393d098a74b6c93ad53c08fc7c51dd76b07fb3" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryContentService/gpg_key_test.go", + "sha256": "2dd55f3c0beeeba8dd983fe907fb80fb56bc6bc5c132449175f7924e0dd0f040" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryContentService/internal/admin.go", + "sha256": "8fb754c1c2e44c529f9129b1de484b2e3c531d32f220b830f19bf7221d0594ae" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryContentService/migration.go", + "sha256": "85234a3e462f84e81d9581e411447444cf19f10f96ff638bfb925958307bcf1a" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryContentService/migration_integration_test.go", + "sha256": "ac8a2377265b04f2b553eaadf299ec3a1c2056b3cbcf00531ce156c5fd28a570" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryContentService/package_delete.go", + "sha256": "370ec47dde5ab86820dca24aac58b1e9de40dc0f180b071714a3ac34a41b3b89" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryContentService/package_delete_test.go", + "sha256": "b35b7ff3f63dcca13b240286ad235adb110ef38ada435332c73256852e4bf68e" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryContentService/remote_upload_retry.go", + "sha256": "5d2883bbc45f03a00696883edf65bc8c72ff1ace99efc3210db8c057164f93ec" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryContentService/remote_upload_retry_test.go", + "sha256": "c883d3620635ff52aeff8bd94cca62ed0828d8dc8ad4b26834320a5fd7f54b2f" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryContentService/repo_config.go", + "sha256": "9110002722de7252cc9ceff4d9df2eecac22ea28489eaaa2c4f90785347d3ff1" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go", + "sha256": "c5d0f9a856e255907dc9629311ffd543dc9c5173c3f2f39a270c67f274e559a8" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryContentService/report.go", + "sha256": "36aa0f86d8333d2e09ef8d023583ce708e9af8a0d7b016c5a90eee13de4d9ee4" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryContentService/report_integration_test.go", + "sha256": "6de79307749feb99ae4add39e7c0a75f2168dbd501d1c87544112ffee1dbda12" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryContentService/report_test.go", + "sha256": "a1e81b0776d50495c00199e0bb4fb371460ab928a8ea843ae5c0758c5843fa06" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryContentService/service.go", + "sha256": "580c1962c31e3f41abf0a9ce33d8538910fd090926389ca5514024e8909219dd" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryContentService/system_repository_test.go", + "sha256": "d95b0723beb653c1ba19f22f2e762f6f2d063e3a5c62cdfd75b9280f9d372244" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryContentService/upload_session_test.go", + "sha256": "9338153107ebccdac713c8ebbb367504d3b4801dc9f76348fa57a291fb497299" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryDiagnosisService/service.go", + "sha256": "2bfd87d9a85fda6e2cf74a39b6d87050964e3bb351bebba98a558aab7e42952e" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryDiagnosisService/service_test.go", + "sha256": "bf72d5a63cce833dfbf6e623db792bb4b6f6efa02d3fcc71ea2c04372773d7f6" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryDistributionService/report.go", + "sha256": "24165a1852914899af195f50dc60821006b754319a3e1da79802bbad25fb8d2b" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryDistributionService/report_integration_test.go", + "sha256": "3293862a00bc3e0ba4d85434c081d14a0e53e2d8cf12757858d7898cc21ec9ed" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryDistributionService/service.go", + "sha256": "adf2f3fe7c1acd018e089b29d529faf65b584940708eed6568c9ea0687c21058" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryDistributionService/service_test.go", + "sha256": "7035846b33a4454f3ce8cabb1b608cb8933e006e1f932f80036d2cbacc1e0fdb" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryErrorCatalog/catalog.go", + "sha256": "66c5f558e80eeb4d1fd6343ca4c91a1dec07490c1cd7efe2044db87cf9f02187" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryErrorCatalog/catalog_test.go", + "sha256": "b85a75d075e2a2ce2aaf20bd0173d8ca268ff5969e34819b93922f6ed4a8b8fd" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryInventoryService/service.go", + "sha256": "6926cbfdfc74eb9dd24849fbca1128ea5ef3fede00f15b9cbcc35e5de93575e2" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryInventoryService/service_test.go", + "sha256": "2a20c7d632be6138106b91f1a1e5864171bf5b2133bd3ecdc8a125dbca168532" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryNodePlatform/architecture.go", + "sha256": "f8eb132f15a3ed078991ab37581143c7077ca1736a078b16ec180eca0f21f7a5" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryNodePlatform/doc.go", + "sha256": "053a9f7247b30d446fc937081cb2bd400232d6ab48d994abf33ea48f4a1c9be1" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryNodePlatform/evr.go", + "sha256": "c3ac183c3b9ca2c3641010cc2f6a66ab0583afb878cc86d4c61589a0511c1a5c" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryNodePlatform/platform_test.go", + "sha256": "7b4482d857e622c836e859d0c76bfbde1e3c9154326250392bc6349cc8c34e59" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle.go", + "sha256": "51821da2031b988e6e9e754fd3d6f4552ab1099f231af3a28f00a4cc78d2d513" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_test.go", + "sha256": "c151808fb6b0ed7dfad08c1aa523ea2935cf2ec68aeaddc9fe6d9dbf1af7bb4f" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryNodeService/liveness_integration_test.go", + "sha256": "ef15628735afc253608b3e21cf9ed1de05d18e72c7aab308da81ea22a84a8339" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryNodeService/liveness_test.go", + "sha256": "341b72bcd062e61bbfcb1372c38d0b7bfb20b80c46be26375d952f3c9b182dc5" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm.go", + "sha256": "c1411563f0cceb94ed2174df1ded6da2538b6d558145892dadc869e5a593a3d9" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryNodeService/node_rpm_test.go", + "sha256": "253436e76ef67fd070c380eefafb0f69d9ed6bde5a480290ff91a7565f7d9530" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryNodeService/offline_context_test.go", + "sha256": "0744cccc205b9ba77f6f61fe98521909f70b48516a4004c36d5f7df63d4e05a2" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryNodeService/service.go", + "sha256": "e6892fde03c4a15526e79231ca8e5e841798326496ed65fe0773ebf42a9a6d7c" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryNodeService/service_test.go", + "sha256": "3c8a40479c95f7b2eb3a5092e5ae7b80c1689f682bf651bfc24c73432633500c" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryPackageInstallService/preview_artifact_linux_test.go", + "sha256": "32d7d9fa09110ddde0d5ee90e0ebe2c1253fb7f3c0000de5a6733c8d58adf33c" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryPackageInstallService/service.go", + "sha256": "18eb9a8872e5009bfddfdb42036ef6544f4b86ce8da1a7b76a7bb39c50838619" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryPackageInstallService/service_test.go", + "sha256": "2cea3bff3b17247653896e1db33692d33aeee8b28bc348ec673bae260a1add9d" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryScheduleService/service.go", + "sha256": "6243fc91257a4593958016f840372ba9f41db69fd5fc2b501961f520ef4b504e" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryScheduleService/service_integration_test.go", + "sha256": "c5457fd0527eafbdc06a4d7c4720a2ef11119c2f12f9f6554e76440e158667dc" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryScheduleService/service_test.go", + "sha256": "c37baf867b30f01dea8d692f5df71f9f10ee30df5640d9a5087fdfde17a0ab26" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryService/service.go", + "sha256": "6415bee2949c57c2b16724db433a97350768a342017ad9325e846a02533161d5" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryService/service_integration_test.go", + "sha256": "ea8df6d2cd864af189c4fa77e4669dfdb2feabf72e9b57e206aff508b75e639b" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryService/service_test.go", + "sha256": "fe426e907162579a9a08576ac46099f8cb3f32c5d339f799d560f6c5ac038650" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryTaskService/recovery_context_test.go", + "sha256": "0aa89e449b172c4b4ae252e723265d17df7739e3898cb1a8436aca20892d1b4a" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryTaskService/service.go", + "sha256": "e23dc1b66ee65b5758e888836365f39c8b309591c0b89efbe6c02c0bf0799ff6" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryTaskService/service_integration_test.go", + "sha256": "66a2afbfc8663114706f5505d6417ff13c459d4d93070171c62ec07084155d80" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryTaskService/service_test.go", + "sha256": "347c5722d7e6238a96191b3479c7e208c57ca02e315bab4d80782d618c2453c0" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryUpstreamService/service.go", + "sha256": "ee4413e7af205ff854ecd143bc012f31219a60bd64eecfc4a4a54356dfc317b9" + }, + { + "path": "manager/backend/services/footstone/domain/repositoryUpstreamService/service_test.go", + "sha256": "03a70a5e548cf025d00390eb5858013e703c811c95aac33bd0d861f241d5ee59" + }, + { + "path": "manager/backend/services/footstone/domain/userService/login_security.go", + "sha256": "17baac9555cc3e5549c534b2c963ec0b829965551c97e8b22b7c081952bc100d" + }, + { + "path": "manager/backend/services/footstone/domain/userService/login_security_test.go", + "sha256": "3a15273ea6c6aeff404554637fb3a33d0b77cedf60203fc448b2921edea92d82" + }, + { + "path": "manager/backend/services/footstone/domain/userService/user.go", + "sha256": "7e7fc99d5f9223ce50e5d08ac07fa470203cb07882f8c4c7f5c190b28b4c0328" + }, + { + "path": "manager/backend/services/footstone/domain/userService/user_test.go", + "sha256": "cfde50532ce6c10e89f3a71806b7955ef22e00741a65074ae2978c8a2e1cb9c8" + }, + { + "path": "manager/backend/services/footstone/domain/userService/userpasswd.go", + "sha256": "1ec5b19c3b7e0573afce833bcdc7616c9e30f580d542fd04c90682a9539970c2" + }, + { + "path": "manager/backend/services/footstone/infrastructure/config/config.go", + "sha256": "488054ec0ec5a004320d235eb43f73bbf3c4ec14c4665409b4b67383b4b21a6b" + }, + { + "path": "manager/backend/services/footstone/infrastructure/config/load.go", + "sha256": "76c23af39641ff2d9685efe554c09e88b8785f8367f945422883df04d14f8b63" + }, + { + "path": "manager/backend/services/footstone/infrastructure/config/load_test.go", + "sha256": "b143123b263d0d866c42965b362003d48b3b33452685d13a43beba7fc3fca8c3" + }, + { + "path": "manager/backend/services/footstone/infrastructure/config/logger.go", + "sha256": "c4d057c868ce6bef4719365335340a6b326023465b73a6e586713fc6df51229c" + }, + { + "path": "manager/backend/services/footstone/infrastructure/distributed-cache/cache.go", + "sha256": "478d1593142f92777a00b6eebdc9537f256172dad0cb9d2af9c4fcad1bd4fd9d" + }, + { + "path": "manager/backend/services/footstone/infrastructure/distributed-cache/cache_test.go", + "sha256": "6f03df9bccfaaeeb49d0c156ac1b69d39d84d567812059905c79e18e14982915" + }, + { + "path": "manager/backend/services/footstone/infrastructure/distributed-cache/redis.go", + "sha256": "7737c2a14c3d16c49ae61c6c8df8fb73d99e6780f77dd4c5d4082ec9afde053e" + }, + { + "path": "manager/backend/services/footstone/infrastructure/err/error_stack.go", + "sha256": "bbc1923ca204ec3b3014896ac0e673c8e2735dd320afe09a166d4c7aaa8f5d81" + }, + { + "path": "manager/backend/services/footstone/infrastructure/lock/lock.go", + "sha256": "1de84a936acd0bf007d6f355a8486b42ba753e882623f371c1e211fb253e575a" + }, + { + "path": "manager/backend/services/footstone/infrastructure/lock/lock_test.go", + "sha256": "5c08cfd6019582836b3424d9c580095441ed072b1d6f5d515d9a55440020f1ef" + }, + { + "path": "manager/backend/services/footstone/infrastructure/lock/redis.go", + "sha256": "16ce79743c549589301c26dd1dadab12ad5e528d96b9403e39c88ab53e2c3912" + }, + { + "path": "manager/backend/services/footstone/infrastructure/lock/watchdog.go", + "sha256": "4e7ec9f4a6647eede7fbca34eed92f4ce0e2f6b903972dfd50a80daf83ea8fb9" + }, + { + "path": "manager/backend/services/footstone/infrastructure/lock/watchdog_test.go", + "sha256": "30919f11323842cd7dbee8d03a3d2759494b26cfba0af8cdd71b5f0c545aaa7c" + }, + { + "path": "manager/backend/services/footstone/infrastructure/log/tm_log.go", + "sha256": "472709e1fb4cb33df3c24f8ec9e410cf49d6a5efcf7f93c5c3bca474537c8bd5" + }, + { + "path": "manager/backend/services/footstone/infrastructure/log/tm_log_test.go", + "sha256": "04ce7a25c7016253b743db6e1a0a2e91fbc8135e18b2452dd94c2faa4566006d" + }, + { + "path": "manager/backend/services/footstone/infrastructure/mq/kafka.go", + "sha256": "38355cd5bbf16c7c7ec761343837ca223149c8279e8ace0e2c8815b13d1aa0ef" + }, + { + "path": "manager/backend/services/footstone/infrastructure/mq/kafka_consumer_group_test.go", + "sha256": "d0b2f2ef1861f2ce5a96520938a2e10204c211731366654b3427eff84ef22ff4" + }, + { + "path": "manager/backend/services/footstone/infrastructure/mq/kafka_test.go", + "sha256": "d0d405b398e450b90136341301124f0ad6f4839a3c1ecd6ce8e302b2936534a5" + }, + { + "path": "manager/backend/services/footstone/infrastructure/mq/mq.go", + "sha256": "2ad293e31c2115983b1d448671e693d3486402b704915750799fc197ad82fe46" + }, + { + "path": "manager/backend/services/footstone/infrastructure/mq/mq_test.go", + "sha256": "9bda0a017c2c918b11c5c4c370c6a9d3b1d40a0a7dc2bfa8c1d397313f049247" + }, + { + "path": "manager/backend/services/footstone/infrastructure/mq/pbmq.go", + "sha256": "7f270ff557387cf029db252752c21bc972eac85676b02faee9b111a5a534fc7e" + }, + { + "path": "manager/backend/services/footstone/infrastructure/mq/pbmq_test.go", + "sha256": "8f85e868e9ca05257caf0379335455a168cacee7e167b621ca21b41cda14e057" + }, + { + "path": "manager/backend/services/footstone/infrastructure/repository/nodeclient/client.go", + "sha256": "7482f1d5126834b004f63bfe1221671500738e703f36ba65fa3d242a31eac8a5" + }, + { + "path": "manager/backend/services/footstone/infrastructure/repository/nodeclient/client_test.go", + "sha256": "ef88f9d6d6e08af4448179ee9a5b0cb6202cb9c3efc862b21087b08a9604d6b6" + }, + { + "path": "manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer.go", + "sha256": "d891e220c4557a8f6b54fe1c3356cf43de95bbf86aff94979fbcc7380e381fec" + }, + { + "path": "manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer_test.go", + "sha256": "c85401b91b5f34fa222c133452e7f42e2e16743191dd9844d97b53024a2272e0" + }, + { + "path": "manager/backend/services/footstone/infrastructure/repository/nodepki/issuer.go", + "sha256": "eba4aeee322ae24a79f265d4013fcd9c414ac9ff2e35da89fbee98f489c100c5" + }, + { + "path": "manager/backend/services/footstone/infrastructure/repository/nodepki/issuer_test.go", + "sha256": "b360add3b2bc2d9ef228d42b7f742364c130f3032df10513f0e5e9fba81561c0" + }, + { + "path": "manager/backend/services/footstone/infrastructure/repository/nodeprotocol/protocol.go", + "sha256": "92afa5786976770da3d532a12534d94d6fcf218ca759391cca9ca4025df8daf5" + }, + { + "path": "manager/backend/services/footstone/infrastructure/repository/storage/capacity_linux.go", + "sha256": "7c1ea1ab9571bec0932b8f3f7da18c4c3452b53e89c8ba387206adc0908eba89" + }, + { + "path": "manager/backend/services/footstone/infrastructure/repository/storage/capacity_other.go", + "sha256": "934b5563083c3acbf8b562ae6f50d91213aae74c2c67abcba3e1351f652dcdaa" + }, + { + "path": "manager/backend/services/footstone/infrastructure/repository/storage/gpg.go", + "sha256": "9cd98eb064c89f3c3ffc908234c67593fd973a5fb0a68edade8d0c943cfcbcd6" + }, + { + "path": "manager/backend/services/footstone/infrastructure/repository/storage/gpg_test.go", + "sha256": "ff817497b8c4028f293cc88a3df8395ef663bb14afabc4761524a03d7298d5e6" + }, + { + "path": "manager/backend/services/footstone/infrastructure/repository/storage/publication.go", + "sha256": "f6a1d123c4f9db95bb0fa0b7c2a586a9e05657d5cc34b97addef705ded974db8" + }, + { + "path": "manager/backend/services/footstone/infrastructure/repository/storage/publication_test.go", + "sha256": "b560daa0dcddebbb091213ed4a72668644f9a73c9a8b94e3fd72982bc74c95f0" + }, + { + "path": "manager/backend/services/footstone/infrastructure/repository/storage/storage.go", + "sha256": "a5489a17fcd93e03ac66553d5ec22fe4a88c2e757e00cf4c1d925b82ff825c90" + }, + { + "path": "manager/backend/services/footstone/infrastructure/repository/storage/storage_test.go", + "sha256": "c6ed98dec258ac154e51d9b87273a2d425d82509896f406bda15267e1575ae3f" + }, + { + "path": "manager/backend/services/footstone/infrastructure/repository/tasklog/large_log_integration_test.go", + "sha256": "53b458c3eb44cd189f7288f403d9fcd7ab4eebe17fe4be0489ed414f6527ac89" + }, + { + "path": "manager/backend/services/footstone/infrastructure/repository/tasklog/tasklog.go", + "sha256": "78d57a4d653cc8b8ad85919ec285462da9ee4391d101eeae4064fbf24268be04" + }, + { + "path": "manager/backend/services/footstone/infrastructure/repository/tasklog/tasklog_test.go", + "sha256": "a66125cca9117424e07ab650205ff5ad55d7571ff9e37c8e89a0854e823a7010" + }, + { + "path": "manager/backend/services/footstone/infrastructure/repository/taskresult/failure.go", + "sha256": "a568b820ac17b530e1b1b3b9ff92270b5d132f2627982d024b70a340839b2815" + }, + { + "path": "manager/backend/services/footstone/infrastructure/repository/taskresult/failure_test.go", + "sha256": "7acfc3769527b5c3f24a5d76cea3538e55fd5ad117e81507d186ca67d6704d46" + }, + { + "path": "manager/backend/services/footstone/infrastructure/repository/taskresult/result.go", + "sha256": "e3a8fe6e0630032a4329daa2f966db2383418dedaa1b8344fb84a7424f9ba73e" + }, + { + "path": "manager/backend/services/footstone/infrastructure/repository/taskresult/result_test.go", + "sha256": "be13372ef1f18a0b1f687ca319c688f820e74907787f6e7340c3505f4ec9924e" + }, + { + "path": "manager/backend/services/footstone/infrastructure/repository/tmsclient/client.go", + "sha256": "cb5148bbde335ea174c1652f7ecb452fe22cef838749e64e0c21026867b9ea3c" + }, + { + "path": "manager/backend/services/footstone/infrastructure/repository/tmsclient/client_test.go", + "sha256": "a575a37c4f565c86c6dba851add09870185d7b55ec294bc10341d680fc5af3c7" + }, + { + "path": "manager/backend/services/footstone/infrastructure/trpc/http.go", + "sha256": "811844b705123e61f0b61908bf510ee083f382944d888e6253add8a8bc1a0430" + }, + { + "path": "manager/backend/services/footstone/infrastructure/trpc/http_test.go", + "sha256": "3a4afb10f1325928be46456250e64f0433be7dd7d3871bd3b6950f1f18d4afc8" + }, + { + "path": "manager/backend/services/footstone/kafka/topics/v1.0.0/sysinfo_collector.json", + "sha256": "cecec51c88b969ed4d2587cc8c1bd9bf1b327e6d66d779f9b3f6b4718629412e" + }, + { + "path": "manager/backend/services/footstone/pkg/library/jwt/jwt.go", + "sha256": "6f56a8eccd685ce945b05ecb0819f5d29d3ab777cac50f9c14121937d1684e4f" + }, + { + "path": "manager/backend/services/footstone/pkg/library/jwt/jwt_test.go", + "sha256": "b9c88373b3a96700f14b98344c432bc6f7e0c89dcad0ff93b882855458823a4b" + }, + { + "path": "manager/backend/services/footstone/pkg/library/toolkit/sha.go", + "sha256": "438ccd7ed0deaac531697b0d784314741865f33eb085db6364fae70edd3129e4" + }, + { + "path": "manager/backend/services/footstone/pkg/library/utils/utils.go", + "sha256": "e1044e0dfda0fc57c53098210d8ea62945153422d2ac1c54dfe944ff42c7b050" + }, + { + "path": "manager/backend/services/footstone/presentation/Instance_base_controller.go", + "sha256": "fe7419fa852402805f92710317abaf2f22519d00ecdff95ad7b1ca2e49410afe" + }, + { + "path": "manager/backend/services/footstone/presentation/audit_controller.go", + "sha256": "daf5637da57a11b113fe1c484a225e79c2d4a6c2ff2eb7387f4e4f9d950123e9" + }, + { + "path": "manager/backend/services/footstone/presentation/audit_controller_test.go", + "sha256": "78eea8c5f297ed0568b01a8e337cb2e317ea77a06974c8ebf99e7ae3038d2bba" + }, + { + "path": "manager/backend/services/footstone/presentation/basecontroller.go", + "sha256": "685e2720b3e164af5366fe868911f9a91706d1cd2775b65b7acead4f6dc3e2cf" + }, + { + "path": "manager/backend/services/footstone/presentation/basecontroller_test.go", + "sha256": "3d6daa6175372f0f746cd80ac59b69d6e7daed392f7afd1549362baf12a1ea1e" + }, + { + "path": "manager/backend/services/footstone/presentation/cluster_controller.go", + "sha256": "6a237a84d8b3300c6a4b03073438a395db8596b70cd609968506255d870fb73a" + }, + { + "path": "manager/backend/services/footstone/presentation/cluster_controller_test.go", + "sha256": "9c9fcc08cc8e5a55ace775be870200f41e49b168d43a1611f938ef698dcb110f" + }, + { + "path": "manager/backend/services/footstone/presentation/footstone_controller.go", + "sha256": "28bdff2404d0bdc22c81815a27719abb16af38c8f365585689f138db286965e4" + }, + { + "path": "manager/backend/services/footstone/presentation/footstone_controller_test.go", + "sha256": "e69d27fbc116ade71cd015a86b4d3a2e52f1addb474f66d6e63601e95711739e" + }, + { + "path": "manager/backend/services/footstone/presentation/has_instances_test.go", + "sha256": "3dc0f64352ef68b0110bbd2192bf6fa8cee95783546cb0c3054b058932bcef3c" + }, + { + "path": "manager/backend/services/footstone/presentation/infrastructure_controller.go", + "sha256": "ad1f97920f0219a6482576b7eeb6a8c978b0992f79d5c9d3a79997834094a8b7" + }, + { + "path": "manager/backend/services/footstone/presentation/instance_controller.go", + "sha256": "beac70ec4c045625c7c6354e20d738911d90fb14f42845f65cccddee691e0127" + }, + { + "path": "manager/backend/services/footstone/presentation/instance_controller_test.go", + "sha256": "7f9145c4f63813e2e82e5d4827ae099ce49e2b6c156cfb6accab3c7e9a15e457" + }, + { + "path": "manager/backend/services/footstone/presentation/metrics_meta_controller.go", + "sha256": "34dc2e073849b496e19dec620d25f756780760176eb664b5229bbf1e6afce2f8" + }, + { + "path": "manager/backend/services/footstone/presentation/permission_controller.go", + "sha256": "7df8ae6b98440d67255df78c29670d1c77adf35b42b31669550d68ec6c0af0cc" + }, + { + "path": "manager/backend/services/footstone/presentation/permission_controller_repository_test.go", + "sha256": "2acd6b83be21108c8b613d122d8c7e34945fa304aa0f85f8e6dfbc33a6516a7b" + }, + { + "path": "manager/backend/services/footstone/presentation/project_controller.go", + "sha256": "d5b0a580986d51514e16d409ed763a72ea214f25f6d27ad3283c6de960582c62" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_content_controller.go", + "sha256": "65a27f3693b4ae74d158256b3d8563624d2658dbdec0c742277e718be8b2fd0f" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_content_controller_test.go", + "sha256": "0126b69db136c79314dc08d3bede0a96f4218a54a1aea5e2c33eaca9e81e1d28" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_controller.go", + "sha256": "e6b378001b516a75a8b406a1d96307e62304324ddcbe9b49f4d395071db89e7e" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_controller_test.go", + "sha256": "e9f32d269da9ee73d4ff0c374e01a3844cd33737d6eaec2b5f63c6dffe8d5868" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_distribution_controller.go", + "sha256": "40cd063680d4bd2a0465806f79977372070b7197a104ded01f7a63ab4ebb421e" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_distribution_controller_test.go", + "sha256": "9532f270905e51c69e2329dfe3455b5f8310ac142facc2c3e994c8358e8d4b79" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_error_catalog.go", + "sha256": "f13499ca3e8201958b7e779f7df08bcaf46a75b62e7f449ce26fc943f281602b" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_error_catalog_test.go", + "sha256": "2cacf110b3e1a347f556aea988391e4bd18dbc62f3e3805e14d65fa6865e9e70" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_node_bootstrap_controller.go", + "sha256": "c1ec738b21004800d5d6775034a3d6e35f4f9573dd0285803c4bec352e8a3965" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_node_bootstrap_server.go", + "sha256": "3c338220f628b3565976ae0bca3296c518ef5da7cf630cd28ac4a5382788ef2a" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_node_controller.go", + "sha256": "fe021e6c57d58e77191b40013539590b48e8edc5ab23364cd07954ac2ec66514" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_node_cross_host_integration_test.go", + "sha256": "3b571d64dc322d3cbd773b19d6ca47f04935ac931712fcb998243d6fcb65dbea" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_node_internal_controller.go", + "sha256": "631e9418566d6ae5198a730e7047451ed50b4b63d46b03f3263029e6b926c1b3" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_node_internal_controller_gpg_test.go", + "sha256": "180d716cf2c52439a8a848edbbbfdf6afc573b921b7e33c0376f8713090b812d" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_node_internal_server.go", + "sha256": "fded63afd5f81c96ebac75c6f4b6c12c5a59b7e8999636b01c9f4a513ea3ae69" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_node_internal_server_test.go", + "sha256": "98601262ea7c3de54f7a882f11246a825e2bc70c110a36f383df27d520573fec" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_node_recovery_transport_test.go", + "sha256": "d116a32ad84fe2b5541b2eb87839a00414417981111129736de8e3bc800516f2" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_ocai_controller.go", + "sha256": "ff89770a048f3784574ac9a6a74c2d310149f4ba1198f1cf2069bb0c13bfbc9f" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_package_install_controller.go", + "sha256": "f95b04415998b36edf58ca6ccf05c0e59ad37b41d91e3b0e064fa0a528bd2411" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_package_install_controller_test.go", + "sha256": "145719c19acec6e9970ca08e73bb8c499769ebbac9d44d939f4baed0ac54db1a" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_report_controller.go", + "sha256": "a6b73a2e25dd15e191ca2c8b93fb9a48b3c00270383e59dde7cd7297159f265e" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_schedule_controller.go", + "sha256": "45835a6be8b4def007a645536e047ceab83e6f7bcb437b6bf4e6f6646b31d37d" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_task_controller.go", + "sha256": "b8ad23cc3e972d52bc8f567286cbdf7cba93130aea0330f780b396663148ab1c" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_task_controller_test.go", + "sha256": "7a695d630f1e4dca24b85e82939f0bf2d964022028df1487c8224e715680c506" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_upstream_controller.go", + "sha256": "3ec06b9b5abe57ca43f41524815d4822dbebb9f8e44d4019eb0c9c3ef4d7e9eb" + }, + { + "path": "manager/backend/services/footstone/presentation/repository_upstream_controller_test.go", + "sha256": "d0e68639d3e6d21e375105a7914509da74bac98f434905ae648b0fb4cf8697be" + }, + { + "path": "manager/backend/services/footstone/presentation/svc.go", + "sha256": "38347548ae2b84c66b3dd4022dba27bd904a92558e77ff6df120ba3140bed890" + }, + { + "path": "manager/backend/services/footstone/presentation/svc_test.go", + "sha256": "e6026ff02844c2a1b27b5ad4cdfc3e7ad5e3b3ebfba5f126fb763793eac479f7" + }, + { + "path": "manager/backend/services/footstone/presentation/user_controller.go", + "sha256": "1b15818bbb8af2871f2408c14e1a1ac883a168e477a97d5e47e56936174e49d8" + }, + { + "path": "manager/backend/services/footstone/presentation/user_controller_test.go", + "sha256": "ae449eda178ca7a70f907c10ea8f7177cf919183387972ce4fe4700e4c930e09" + }, + { + "path": "manager/backend/services/footstone/presentation/user_favorite_controller.go", + "sha256": "51ca9a8ff5fc46041eaa3fc8726febdcaac5d8fb1de734596f351327602a5c5b" + }, + { + "path": "manager/backend/services/footstone/presentation/user_favorite_controller_test.go", + "sha256": "6058290868f625e6279660263f6a3fd5d85f43aea9d2dce1bcb622856740f4af" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/bootstrap/client.go", + "sha256": "b818a336b6cbc4c326a876b76a912a2a2d5ec39b5eeec670f58355ff91277995" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/bootstrap/client_test.go", + "sha256": "947c6bcdfaac2fa6d209ffbbfadc44b47006939c9ee8680b51c46a6764d2abe2" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/command_log.go", + "sha256": "46a4bee003b9236d882916a0b5db19e2643cba98153d85c96d235ee1c58a1274" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/command_log_test.go", + "sha256": "302e6ab7f80d06c2dce02c9bcdda5c9da98b8dce15d413727aceed503bf9c988" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/controlplane/client.go", + "sha256": "43318e2a416fcb11e7adb45a671b8bb95dac0193bcd5794f3fb4503c232fb905" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/controlplane/client_test.go", + "sha256": "eccf4dd83dc794db6e9f25834cfcf0acf6bd3d33584032116968c5d6b75aa653" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/distribution.go", + "sha256": "601503554ce926c1465510d740a3482d76175eef1d4e2d3cf069c50642fda761" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/distribution_integration_test.go", + "sha256": "b34a5aef0707bb43bbcca7902095b55c4d99ed61f8c712ea8cfca97e5a0ab746" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/distribution_test.go", + "sha256": "ebbbe16c6664cef1115eea9c4ca0869ecbacf3a767539cbd42914e84a606d115" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/dnf_e2e_integration_test.go", + "sha256": "96d58c3c0d0e3a02dd936482bb3cad91e6d98e3b70cc391685f1ba496d7f16b6" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/gpg.go", + "sha256": "993b6cc174ec1def0726f5bdc44b4ef45753c90ce3c7f6f1ed21234a6fff8be5" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/gpg_integration_test.go", + "sha256": "ae3cad76eaf458bb1e15f2ec7d9d28ded3d6207b62e8848b58a254851558c920" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/ingestion.go", + "sha256": "2931603b2e4fcdf6550c4a0cce2f50c312904f03ddc857211e93153b57741f7f" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/ingestion_integration_test.go", + "sha256": "b8b2de80f2ea205eb6770580c3bcc83cbd8616357b237605c510281d38900917" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/ingestion_test.go", + "sha256": "4af6db28a8f7b24751c1497f711406475157001450c03753435f851daa100ad0" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/logging_executor.go", + "sha256": "7e26d7827cc658bbe772aa4abceb669859fdc440d5fb787c334ac04127d37da6" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/logging_executor_test.go", + "sha256": "263d908ac3a65e8f72fd1ad0c4d87a01cc164d99db6243da91923f6b3c1eb452" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/metadata_signing.go", + "sha256": "26f00cdc4e2adbf1260fd747a2f4bca80d25eee135b42a79c263d0edd5721fbc" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/metadata_signing_keys.go", + "sha256": "666aed29171caede6aaa461bfb64082733902ed4dc9e2ebef86f624d003f6575" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/metadata_signing_test.go", + "sha256": "8cf5e852e1a1521577c24266cb9c256bce30637ce647373d930f8e7be830380d" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/node_probe.go", + "sha256": "5cfee73d8d25fd761721fa66bd37776037a92c378eda4838baaf94bd94931459" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/node_probe_test.go", + "sha256": "e319740ffcd4314ad86fbefe8cda736c3ab7a7b9cd632ee9691e23355b92ea16" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/nodeapi/server.go", + "sha256": "afda12b1131f2de0e4cc7ce4a9bbb44fef45a22c8c6d2bd51bcef919a9887c30" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/nodeapi/server_test.go", + "sha256": "72d04f7fa28f7c6d3a8e6655146a75fc866b0a0012c8d94537b6681711ac3006" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/publication.go", + "sha256": "7f14cb6f0580c6d45a891ae7065e854ef87723fc08a31e692b4c6bd5a5687424" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/remote_delete_executor.go", + "sha256": "a48ab8abfab3545695b0195059461c53ad0c8dd3fce98c0c7fd59c2c7c31a924" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/remote_delete_executor_test.go", + "sha256": "88a51d874a511d888e137ba3301ca050d2b37e8c952d8c7469c687aed2ecf8b9" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/remote_executor.go", + "sha256": "9d42ff18622f4c52f21fb8a5907a21706b9012e957ad2906b4a66a059a93e1e1" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/remote_gpg_integration_test.go", + "sha256": "27ecfde65c59e52fae8aeb85d56e5516bb18c9d1309ea54e71bcc488b019b841" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/remote_migration_executor.go", + "sha256": "01ed77db60cfa8c40e410fd2cf84c677703bd7059eaa6f00f17719afdb869f42" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/remote_migration_executor_test.go", + "sha256": "38e2008e2da40a75ad562e32b4e9a8877f542a23c4ee13f49c40776c60c89111" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/remote_migration_gpg_integration_test.go", + "sha256": "cbbb5425cc895a06ba6690d086be954f8cd090503d3cdc4afd22f2c975e7c903" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/remote_sync_executor.go", + "sha256": "6d2fca4989a114c667a44b030c3e478b9457009c755656e6d87c3499b4e36a4b" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/remote_sync_executor_test.go", + "sha256": "001c60acb17b9cf3b440efc67e656e6079d916210775afec42234bd36889c971" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/remote_upload_executor.go", + "sha256": "a144352b58190e856f41199997a1ced8bdf0a447ce26d6388418070f3e57cc2e" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/remote_upload_executor_test.go", + "sha256": "49bd0864a84d5eaa8f27761a2a86a4e3b4ca408b1a50e8b0ad97a49550ce529c" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/remote_worker.go", + "sha256": "b274744356095a7b002adfe1614a761b0d8e1e0aa8e8a21bca436f119d92685f" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/remote_worker_recovery_test.go", + "sha256": "373afecb13461466222434f62e69c0929deb4b896ffc6a94a1ea62829e037bc9" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/remote_worker_test.go", + "sha256": "66bcc7006e5555f3b4959640f154570023ddb5ae649bd2f07fe2a7823b6c888e" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/repository_usage.go", + "sha256": "56d22d2e07e7800a00d2fb00af6b65375dd7392705116fcb6ccfda4b9a9a1295" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/repository_usage_linux.go", + "sha256": "b769fe4bf5ebf8d02e1f31e40ee7894b61669c0909705daf16c09a9f76c5c3e5" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/repository_usage_other.go", + "sha256": "3cd9d4b2f1bc00fd7ecb65e33233dac1a6606b05a25b8236a0b54ec5600d03f9" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/repository_usage_test.go", + "sha256": "4cb1e71d06750ee1ae03abe653ec1d2711b2d52fa37d7127a2da1ff5708000ed" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/runner/main.go", + "sha256": "513e07f4cee5de2a93290233ba42ded3f15073f4e9375d2701a42c3423b67535" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/runner/main_test.go", + "sha256": "b1b0deffb68654deea1db31f7036cd0e746360d53385281a9456b3bef4bd4164" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/sync.go", + "sha256": "53820b2fe5f3f966ad2e9ea0b2b2e21e996967048d5d58300b4cf462cf2cd2cc" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/sync_command_test.go", + "sha256": "db363ad4ff9acae11ee8d3aec0ac2ab86d99b53dc08ebcba5ee4def3d697d766" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/sync_integration_test.go", + "sha256": "f04097c5d0a40982a991d56e00cd89345253b2dc91dc6dd5ce034b429b3e4a25" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/testdata/e2e/ocm-private-app.spec", + "sha256": "aa603d17fcbd7610e809cecf6da1a69f213dce512b6b6d851355ec8c564a741e" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/testdata/e2e/ocm-private-only.spec", + "sha256": "5da341160b6079a566f3727bd4f85cdd78912e02cd3dccfa8df595a105d856fe" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/testdata/e2e/ocm-sync-dependency.spec", + "sha256": "5dbbc7ef60e5d0a7a3caeb1958d4cafd81aa8d8f7569c6c169d7b86db9e07d75" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/testdata/e2e/ocm-sync-only.spec", + "sha256": "85cefd84e9eb7fcc6b82ef97af8feced625271d8cb5d42cdaf2eca5e6f78baa4" + }, + { + "path": "manager/backend/services/footstone/repositoryworker/worker.go", + "sha256": "f129e44497a4c74d4da82657cb0c66451faec670a9d672112b8089710a3869be" + }, + { + "path": "manager/backend/services/footstone/runner/app.go", + "sha256": "4b9e150744c30f8e4c8da031938ab54c348106d5c1235df8ee6a060d9ead435e" + }, + { + "path": "manager/backend/services/footstone/runner/app_test.go", + "sha256": "2044113b1a3063f35d2f580522c03b2ab4798c9603cbdd30693659a3de8b1244" + }, + { + "path": "manager/backend/services/footstone/runner/flags.go", + "sha256": "d302e71a65916a453bc5084e27c3c35175ca7ae7c2013253c372b6da2df6319c" + }, + { + "path": "manager/backend/services/footstone/runner/run.go", + "sha256": "4d06559d59e31faff398cef9545fd2cc13cb9308e2986f5ac5e862a1afbf0653" + }, + { + "path": "manager/backend/services/footstone/runner/run_test.go", + "sha256": "bba239cc2dc6e106841bd56ed23433f8336a223046fe1ff5ca42d5a19b79e76a" + }, + { + "path": "manager/backend/services/footstone/streamworker/monitor_sweeper.go", + "sha256": "9a1c83be5a9aabd06f64d0638c4ef2c0893ce642ce4ad7c777628565339f4388" + }, + { + "path": "manager/backend/services/footstone/streamworker/mq/sysinfo_work.go", + "sha256": "05783f1c0fa98e02bfb4bf3d6a1a68228a5b42936b7859029014360db14a27ba" + }, + { + "path": "manager/backend/services/footstone/streamworker/mq/sysinfo_work_test.go", + "sha256": "7c05da11898086dceffac17451e388c5e7314301981aa8538b03fe2bc6517a6c" + }, + { + "path": "manager/backend/services/footstone/streamworker/worker.go", + "sha256": "940331896461e43a3bc36ee02b119d213020245ee78744630f4a96eedcd52d6e" + }, + { + "path": "manager/frontend/package-lock.json", + "sha256": "cf3f0c0dd9eadf8fe2358241d44e2ec087a20ba0f72d288a51013b7f42ead0ea" + }, + { + "path": "manager/frontend/src/pages/repository/analysis-focus.ts", + "sha256": "8458bbe58e5eb28b44eb7d99c119636176b309ecb9ea2861d7d8c6148f0c7035" + }, + { + "path": "manager/frontend/src/pages/repository/analysis.test.ts", + "sha256": "3a774128e4d4981832e2e36cc7a9e5e7bf6748b7bb3dbf320a3f6799bb3a4df7" + }, + { + "path": "manager/frontend/src/pages/repository/components/RepositoryColumnSettings.test.ts", + "sha256": "429b4140649ecdc7560cce4659a8cb4129881bb1ac73169afd457cad2569507f" + }, + { + "path": "manager/frontend/src/pages/repository/components/RepositoryColumnSettings.vue", + "sha256": "83c4386faeaeaf75c242dce619b94ffdae116c472e966654bc972bac172f3ee6" + }, + { + "path": "manager/frontend/src/pages/repository/components/RepositoryDistribution.vue", + "sha256": "a13d64df3b1dd774c035de1a9f2f5299dd829c4fbdc6dd4b046fbd245a741d6e" + }, + { + "path": "manager/frontend/src/pages/repository/components/RepositoryFilterBar.test.ts", + "sha256": "f48c85ca5da94ec95c4c65ed7ce6806655d9f46b0f812279a90b470b15144f68" + }, + { + "path": "manager/frontend/src/pages/repository/components/RepositoryFilterBar.vue", + "sha256": "cb7979650de7fcdf1c806860dfa47f8b28d349d459a19f6cbab3854bd021effc" + }, + { + "path": "manager/frontend/src/pages/repository/components/RepositoryForm.vue", + "sha256": "d28932d0f8e51c4296cdcb729f24e5255e049af371c8a6fa058800a846108a00" + }, + { + "path": "manager/frontend/src/pages/repository/components/RepositoryGPGKeys.vue", + "sha256": "82ad89a975e19515edab95f4fc958d76f16bba9a1244a4e5cba9023f43090fab" + }, + { + "path": "manager/frontend/src/pages/repository/components/RepositoryNodeForm.vue", + "sha256": "ecee1a048577dc2ea694a2b7ffe22f67106c6bf8f7a650f22aee88e5ab3623af" + }, + { + "path": "manager/frontend/src/pages/repository/components/RepositoryNodeInstallManager.vue", + "sha256": "1e28e6779c245a27be10f749a67057162dbf753cf90fa32c39903e8afcc4c6bb" + }, + { + "path": "manager/frontend/src/pages/repository/components/RepositoryOCAIAnalysisPanel.vue", + "sha256": "986578b132058e28f0f195ae89e3c31a91f5bc5cb7503d8924821a94d6a55e78" + }, + { + "path": "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.test.ts", + "sha256": "3da6bd2f280390331a935232ceac7c7174d3deabd98537723d329dac862b2742" + }, + { + "path": "manager/frontend/src/pages/repository/components/RepositoryPackageInstall.vue", + "sha256": "d8c27ae536aa9d2a6be6602095b30e08f03d894d5c14e49ae621a5081ac6de1c" + }, + { + "path": "manager/frontend/src/pages/repository/components/RepositorySyncSchedulePanel.vue", + "sha256": "0df29b61bf71df79e9a4de01dbe86c66f02d0b8aa0892c8e73aca9083bc474c7" + }, + { + "path": "manager/frontend/src/pages/repository/components/RepositoryUpload.vue", + "sha256": "7c665e2b5706c4f8685c920a23ca2709a2e531b43e2319e03de259d79d924427" + }, + { + "path": "manager/frontend/src/pages/repository/components/RepositoryUpstreamProfileForm.vue", + "sha256": "f8e232bfd4a42ce1b53b1e5af475d1d85de1b566f886c102a4656edef14fa664" + }, + { + "path": "manager/frontend/src/pages/repository/detail.vue", + "sha256": "2ec404d564a726424fc87db5cedc2df9fd2d6472c8b90f99f885a4ac3dd8d51c" + }, + { + "path": "manager/frontend/src/pages/repository/distribution-view.test.ts", + "sha256": "7f74da8886e1b9d7868f788aaf5e7ed3d7d74d7c7c6741da2efdd31fab318b05" + }, + { + "path": "manager/frontend/src/pages/repository/distribution-view.ts", + "sha256": "c4b5c286c3e7626bf0fc248fcd742a518c6c9cde40d780e130787ee77a09c7a5" + }, + { + "path": "manager/frontend/src/pages/repository/index.vue", + "sha256": "d6224d42311492b6bc494ff1d26e3531295a6de6374396e0913c8725b45a61d6" + }, + { + "path": "manager/frontend/src/pages/repository/nodes.vue", + "sha256": "52907e415a6ba62311a1dc30746085ba8a053d9a536980e214438ae0377c66a1" + }, + { + "path": "manager/frontend/src/pages/repository/ocai-panel.test.ts", + "sha256": "be9cc21b69680f32e53fd16bafe283f1c0f8ab52e1c25cdcb8b01043084914ab" + }, + { + "path": "manager/frontend/src/pages/repository/productization.test.ts", + "sha256": "94162973bea8b820143f8b5dfd591f0e87874a929389b1d70dd4a361618344d0" + }, + { + "path": "manager/frontend/src/pages/repository/repository-form-options.test.ts", + "sha256": "4751b80c23c748a5fa1e9e2316d7621380b9462541f957238b7780395567e1ab" + }, + { + "path": "manager/frontend/src/pages/repository/repository-form-options.ts", + "sha256": "e66a15549ec031cfa9fcd157a000ba232a2a3eb976926985633555ee3bc9fe2b" + }, + { + "path": "manager/frontend/src/pages/repository/sources.vue", + "sha256": "bfd336903227f6ffe0cc3da974e3380f7b93bd981218cd7dcdf22a77a01383ba" + }, + { + "path": "manager/frontend/src/pages/repository/table-columns.test.ts", + "sha256": "ccb4081fd56a0a85fb5917cc0a9e45fdafe2c4521f4d45322fbced4bd351d819" + }, + { + "path": "manager/frontend/src/pages/repository/table-columns.ts", + "sha256": "ec126b5f649737b076b0ab4e7c8c529b0b27b646ff9304414ae7f9a6dbe2bbee" + }, + { + "path": "manager/frontend/src/pages/repository/target-nodes.test.ts", + "sha256": "2f5412d9e4812eac180a47f53d705e1edc0b38af0cb39a8248463673c185b771" + }, + { + "path": "manager/frontend/src/pages/repository/target-nodes.ts", + "sha256": "770fb3b19a70892345f71817154c1bebd2813165d500dc229862458683b147be" + }, + { + "path": "manager/frontend/src/pages/repository/tasks.vue", + "sha256": "769ae0828c1a5536efe4cb406668b715cca499a2ae229fd87c0d3bc4a85ace3f" + }, + { + "path": "manager/frontend/src/pages/repository/types.ts", + "sha256": "83b5cbc033ac8582a215ff3a9368cff19632c1673554e23adee87d090bfa05a3" + }, + { + "path": "manager/frontend/src/pages/repository/upload.test.ts", + "sha256": "ac6fee558b660ecf89a2106b22f781fc3c41a4ec9e97317d530dc09e3706c037" + }, + { + "path": "manager/frontend/src/pages/repository/upload.ts", + "sha256": "695b6e345bdf8e426b6d3149e1ffbb4cd068381c1d708b22a6f8452fad273928" + }, + { + "path": "manager/frontend/src/pages/repository/useRepositoryAnalysis.ts", + "sha256": "c601cf608c10c9e95e0f53551fba9a9b386ad2c81c4a050ba97de69286f63462" + }, + { + "path": "manager/frontend/src/pages/repository/useRepositoryTableAffix.test.ts", + "sha256": "9399fdcb68cdb2bd0771edbe67142906cceee939b7b0c6913a2a56524852f78f" + }, + { + "path": "manager/frontend/src/pages/repository/useRepositoryTableAffix.ts", + "sha256": "5f2be51b01e3a31c9362781a6108f3109d3b9a65c7b64b115586a9d5fdd79044" + }, + { + "path": "manager/frontend/src/pages/repository/utils.test.ts", + "sha256": "9186167527e3d6a9eb66c33f232f058187656a9c0061bc43edaa9f1b40214cd2" + }, + { + "path": "manager/frontend/src/pages/repository/utils.ts", + "sha256": "7ac3f39a07c55a6287d9d7ec3825b69549dfbe1f0d966d7f220ef0614281bd76" + } + ], + "validation_scope_status": "PASS", + "full_152_rerun": false, + "final_ready": false, + "not_reexecuted": [ + "aarch64 offline node", + "Node RPM install and allowlist race", + "restart/redeploy/destroy-restore matrix", + "full capacity/rate-limit and Case A/B/C matrix" + ], + "remaining_warnings": [ + "existing Vite chunk-size warning", + "prior upstream tnet race/checkptr failure remains separately recorded" + ], + "release_check": { + "status": "PASS", + "files_scanned": 1670, + "nonempty_scan": true, + "source_manifest_sha256": "323095343b2dd388e36855c40d4cceb5c9c114df9ed078fc2deea9e33bba2461", + "original_zero_file_scan_rejected": true, + "source_archive_verified_file_by_file": true + }, + "documentation": [ + { + "path": "docs/私有化软件仓库方案设计.md", + "sha256": "4b30a0e719cfcb4fdbbcd544abb47afefe5b58732f160b997c01e749cca4e1d9" + }, + { + "path": "docs/私有化软件仓库部署说明.md", + "sha256": "5cf4f8396ce47c6dbf42b6e488d8d1e1ed47d3efb1526c56c3fee936f785d27a" + }, + { + "path": "docs/私有化软件仓库测试验收.md", + "sha256": "99100abe4054736b238f20451c925d2e88a3f8594fb00a9b7eb29c55498b67c4" + }, + { + "path": "docs/私有化软件仓库结构化验收报告.md", + "sha256": "e109e0ee4c6692ff2e8e219448fa583cba7f9a522c07dde925791208cd51ce1a" + } + ], + "final_source_unchanged": true +} diff --git a/docs/evidence/repository-taskflow-validation-20261024.json b/docs/evidence/repository-taskflow-validation-20261024.json new file mode 100644 index 0000000000000000000000000000000000000000..9d36ee6f54217b5ce58b877556c85c38e6b35494 --- /dev/null +++ b/docs/evidence/repository-taskflow-validation-20261024.json @@ -0,0 +1,232 @@ +{ + "schema_version": 1, + "date": "2026-10-24", + "scope": "Fresh product API readback of actual task aggregation and post-rotation health; not the complete release gate", + "status": "PASS", + "task_api": { + "captured_at": "2026-10-24T09:54:07.551Z", + "project_id": 1, + "samples": [ + { + "type": 1, + "total": "52", + "task": { + "id": "e1dab2e3-739b-4aee-9589-12031e193a9e", + "type": 1, + "trigger": "MANUAL", + "status": 5, + "repository_id": "4f04c2d4-77ca-4088-bbcc-fb0060cf51f9", + "storage_node_id": "7ebb8325-6021-4ab5-bd78-2983a3e405ef", + "revision_id": "26ecf0b6-3cfd-42c5-951d-dfae5f3e9436", + "started_at": "2026-10-24T01:32:49.592Z", + "finished_at": "2026-10-24T01:32:50.784Z", + "targets": [] + } + }, + { + "type": 2, + "total": "33", + "task": { + "id": "c85d80f1-8243-4342-a932-221690f7f30c", + "type": 2, + "trigger": "MANUAL", + "status": 5, + "repository_id": "d348dc62-30a4-423e-8c35-4c391a9325ce", + "storage_node_id": "7f9d0711-948a-4b1e-b12a-50d46bbbb157", + "revision_id": "bc0a4327-3aec-4d76-9421-033764150dda", + "started_at": "2026-10-24T01:41:12.455Z", + "finished_at": "2026-10-24T01:41:13.419Z", + "targets": [] + } + }, + { + "type": 3, + "total": "6", + "task": { + "id": "49829438-e21a-445b-8d5f-85091084eb59", + "type": 3, + "trigger": "MANUAL", + "status": 5, + "repository_id": "ff6e0a23-58ed-4a9f-bacd-618f835bc08f", + "storage_node_id": "7f9d0711-948a-4b1e-b12a-50d46bbbb157", + "revision_id": "631f2fc9-2831-4ffd-bc26-5a5a26f71794", + "started_at": "2026-10-23T12:34:51.181Z", + "finished_at": "2026-10-23T12:34:51.430Z", + "targets": [] + } + }, + { + "type": 4, + "total": "0", + "task": null + }, + { + "type": 6, + "total": "0", + "task": null + }, + { + "type": 7, + "total": "0", + "task": null + }, + { + "type": 8, + "total": "13", + "task": { + "id": "5111ade0-0abc-44fa-afd2-1593485c42ab", + "type": 8, + "trigger": "MANUAL", + "status": 5, + "repository_id": "b52a5690-07d1-44e5-a96c-e8ca6d64dbc1", + "storage_node_id": "7f9d0711-948a-4b1e-b12a-50d46bbbb157", + "revision_id": "026c2fbe-9b5e-46bd-8754-5d768dcc9fa8", + "started_at": "2026-10-23T12:33:46.365Z", + "finished_at": "2026-10-23T12:33:48.441Z", + "targets": [] + } + }, + { + "type": 10, + "total": "15", + "task": { + "id": "964ac5c6-0a1f-41cb-a23f-77339cad8770", + "type": 10, + "trigger": "MANUAL", + "status": 5, + "repository_id": "", + "storage_node_id": "7ebb8325-6021-4ab5-bd78-2983a3e405ef", + "revision_id": "", + "started_at": "2026-10-24T01:27:22.849Z", + "finished_at": "2026-10-24T01:29:45.598Z", + "targets": [ + { + "id": "f1b5f68e-8054-46b9-83e5-f64821f5acb9", + "target_type": "TMS_AGENT", + "target_id": "6479597235769020870", + "status": 5 + } + ] + } + }, + { + "type": 11, + "total": "6", + "task": { + "id": "3f64aede-37ff-4a5c-a782-51f86e3f7468", + "type": 11, + "trigger": "MANUAL", + "status": 5, + "repository_id": "2db26715-20bc-4b9f-b228-da2c3b4a8e95", + "storage_node_id": "7f9d0711-948a-4b1e-b12a-50d46bbbb157", + "revision_id": "f574b9a5-ff02-47e1-8510-1b542da9f8f6", + "started_at": "2026-10-24T01:15:07.202Z", + "finished_at": "2026-10-24T01:15:14.285Z", + "targets": [ + { + "id": "051e521d-11c3-4d82-9aa8-a578f3a55e88", + "target_type": "TMS_AGENT", + "target_id": "1177600517345606245", + "status": 5 + }, + { + "id": "e32fad50-d960-474c-88af-9fd5db2795c4", + "target_type": "TMS_AGENT", + "target_id": "685652152724179215", + "status": 5 + } + ] + } + }, + { + "type": 12, + "total": "3", + "task": { + "id": "c785d0eb-9e92-432e-a115-6c94807618e6", + "type": 12, + "trigger": "MANUAL", + "status": 5, + "repository_id": "", + "storage_node_id": "", + "revision_id": "", + "started_at": "2026-10-23T12:34:36.947Z", + "finished_at": "2026-10-23T12:34:36.947Z", + "targets": [] + } + } + ], + "scheduled": { + "total": "2", + "tasks": [ + { + "id": "72d2e178-a7eb-4f36-a061-719ffb0e35d5", + "type": 1, + "trigger": "SCHEDULED", + "status": 5, + "repository_id": "99c4a33a-5d53-4dc8-b6b8-527b44478e47" + }, + { + "id": "51df4982-e0be-4ac9-be67-294b1d1ce55a", + "type": 1, + "trigger": "SCHEDULED", + "status": 5, + "repository_id": "99c4a33a-5d53-4dc8-b6b8-527b44478e47" + } + ] + } + }, + "inventory_job_task_binding": { + "job_id": "4bd17869-a62b-4cf9-8745-b9ad7a94d517", + "job_status": "SUCCESS", + "repository_task_id": "c785d0eb-9e92-432e-a115-6c94807618e6", + "current_task_status": 5, + "identity_matches": true + }, + "node_health": { + "captured_at": "2026-10-24T09:51:51.479Z", + "success": true, + "nodes": [ + { + "id": "7ebb8325-6021-4ab5-bd78-2983a3e405ef", + "name": "p4-arm-node-20260921-053252", + "status": 2, + "version": "0.2.2-0.review20261024.1.oc9", + "architecture": "aarch64" + }, + { + "id": "7f9d0711-948a-4b1e-b12a-50d46bbbb157", + "name": "TEST_REPOSITORY_NODE_X86", + "status": 2, + "version": "0.2.2-0.review20261024.1.oc9", + "architecture": "x86_64" + } + ] + }, + "ocai": { + "completed": true, + "repository_task_count_unchanged": true, + "task_id": "bce358ed-50be-4146-96bd-2f49768ff0bc", + "automatic_mutation_observed": false + }, + "no_sample_task_for_types": [ + 4, + 6, + 7 + ], + "note": "Rebuild/Publish/Validate enum values have no separate historical product task in this project; their internal stages are exercised by actual upload/sync revisions. Do not claim every enum has an independently created sample.", + "evidence": [ + { + "name": "global-task-map.json", + "sha256": "9b8256a8693ba3a1e48ae7a522927387070132e9ba968b86d65f1ef2e7243ad3" + }, + { + "name": "post-rotation-nodes.json", + "sha256": "bc6b476aeff02eb0c3adc607a9a02e0d01208c65146973eb587f4d3adfc7150b" + }, + { + "name": "post-rotation-product-checks.json", + "sha256": "455357fcb660113900194fe7833c30d1542d532a9e4a47b082c397e514869293" + } + ], + "final_ready": false +} diff --git a/docs/evidence/repository-test-portability-validation-20261024.json b/docs/evidence/repository-test-portability-validation-20261024.json new file mode 100644 index 0000000000000000000000000000000000000000..c3116d8a7dc9c6c93802a51a6d2d712535fe4558 --- /dev/null +++ b/docs/evidence/repository-test-portability-validation-20261024.json @@ -0,0 +1,23 @@ +{ + "schema_version": 1, + "date": "2026-10-24", + "scope": "GPG test fixture portability fix only; no business behavior change; full release still running", + "status": "PASS", + "file": "manager/backend/services/footstone/repositoryworker/metadata_signing_test.go", + "before_sha256": "8cf5e852e1a1521577c24266cb9c256bce30637ce647373d930f8e7be830380d", + "after_sha256": "00f077aae76ba62a7b50cd88114a232254c1209b1e6db59e19357a1388a90a64", + "candidate_manifest_sha256": "9488fc0aaf71bbb0078df7f93a976bc18a787e51a1ed4fb2663796c5f081eec9", + "reason": "Go t.TempDir adds the full test name; GnuPG agent Unix socket path can exceed the platform limit. Use a short isolated 0700 keyring and stop only its own agent during cleanup.", + "targeted": { + "platform": "OpenCloudOS 9.6", + "passed": 3, + "failed": 0, + "skipped": 0, + "real_gpg_signature_and_dnf_metadata_verification": true, + "log_sha256": "427d28b4a724dc7a9f427d03e575468cb3072ad76f37c8d29a20eb34bcc4d339" + }, + "runtime_images_changed": false, + "core_protection_relaxed": false, + "raw_summary_sha256": "a5471e1652c04edb2694e27cba11e375d438c32003c6c46b23158500ef5fb357", + "final_ready": false +} diff --git a/docs/evidence/repository-ui-captures-20261015.json b/docs/evidence/repository-ui-captures-20261015.json new file mode 100644 index 0000000000000000000000000000000000000000..9feb243f67d8f0d6c88222a4d562cf9ba625f807 --- /dev/null +++ b/docs/evidence/repository-ui-captures-20261015.json @@ -0,0 +1,101 @@ +{ + "schema_version": 1, + "capture_date": "2026-10-15", + "runtime_frontend_image": "oc-frontend:p13-gap-20261014-v6", + "source_kind": "live_browser_real_backend", + "notes": "Environment names, IPs and capacities in images are sample laboratory data, not installation defaults. Queue capture selected files only; it is not evidence of a new completed upload. OCAI result is an advisory response to an existing failed task.", + "captures": [ + { + "path": "docs/images/repository/repository-release-list.png", + "sha256": "5aecbce2749dc1eb93ceafc08a3a2a27f1ed4ad53bd554880608ec2c27cd3b7c" + }, + { + "path": "docs/images/repository/repository-release-create.png", + "sha256": "227bfe58c92cd2659d7949c5fc810c257d85bed94707d872673b62d1e28ab867" + }, + { + "path": "docs/images/repository/repository-release-create-filter.png", + "sha256": "f94bebb73b2f7715f8e2254d3fbdf676da4cb61a98adc561b8c9d83eae5cc84d" + }, + { + "path": "docs/images/repository/repository-release-create-presets.png", + "sha256": "adef6993328d030f689fa39a808cfcb1dd507a80136e8133df1a640537385581" + }, + { + "path": "docs/images/repository/repository-release-nodes.png", + "sha256": "5062731989d8585a0a29639af11f882a7a78aaef7cb0b14c7d01d0c75d345941" + }, + { + "path": "docs/images/repository/repository-release-node-install.png", + "sha256": "6fc3c5f22f6787738694595f1b70919c6788f1aac93f1d90a1e06f7b10d4ffc8" + }, + { + "path": "docs/images/repository/repository-release-node-sources.png", + "sha256": "81981a341d2f4b3e4d610a73a2702721850539f9d09b9526c0fca20e43c2667d" + }, + { + "path": "docs/images/repository/repository-release-node-allowlist.png", + "sha256": "4f54c542bc1728404d9e101a62d3e1389ada17a2ab1faaa938798fdbe012e99e" + }, + { + "path": "docs/images/repository/repository-release-node-drawer.png", + "sha256": "cd8c14ca03aad9a3ce412caf1357ba0aff1523ef7e09a3713430e9e6440f76e7" + }, + { + "path": "docs/images/repository/repository-release-node-drawer-1440.png", + "sha256": "37946db1b72ec800e03526e3f23ba5916ba9493fcf5a5b55bd11c60b3bb77ffa" + }, + { + "path": "docs/images/repository/repository-release-upstreams.png", + "sha256": "260b587178e624cd839e0fe80435f8e27adf4a89507f39053a4b2e6f85068dce" + }, + { + "path": "docs/images/repository/repository-release-upstream-form.png", + "sha256": "f9e40036e4db5217e2e1ed920ff0cce79bdb90ebfc27640f94f41a1eb197cdb6" + }, + { + "path": "docs/images/repository/repository-release-overview.png", + "sha256": "084d29c22cd1b73c726b3dfc909e6bb53184bce92cbbefff029955547472e3a3" + }, + { + "path": "docs/images/repository/repository-release-packages.png", + "sha256": "b9ae8f85fa0e9725c4452b751a38e0047572a413a23f0e0c76bfa54f35b5da84" + }, + { + "path": "docs/images/repository/repository-release-private-packages.png", + "sha256": "b6761ba1ec870701e7c4b60d5aaca1d1418e23e64adbd95ae7343577934615a0" + }, + { + "path": "docs/images/repository/repository-release-upload.png", + "sha256": "80763581a8c2bb3e7838b1a541b47603208b853efdb7d57524d4ab893755e390" + }, + { + "path": "docs/images/repository/repository-release-quarantine.png", + "sha256": "f08db667e5321387467e36734692954326d0fedef214352c707741f7005a750d" + }, + { + "path": "docs/images/repository/repository-release-config.png", + "sha256": "6f3f1fe7260bfba3ddae57d638b188f3c10298d239487751f4dfd19899f7e85b" + }, + { + "path": "docs/images/repository/repository-release-tasks.png", + "sha256": "dc3411feab68beb0e2bf9b39bf1feab099ec71fa1d6dd5d663ab7f2fa363034a" + }, + { + "path": "docs/images/repository/repository-release-tasks-1440.png", + "sha256": "a010c969ee0ee6589dbd312d479a6b48669eab9e7c958997b3f0b5015618143e" + }, + { + "path": "docs/images/repository/repository-release-ocai-context.png", + "sha256": "bc349154d9faa2d8c34c98fb3874413d7747a2f946cda4f6c2e5372e0a753533" + }, + { + "path": "docs/images/repository/repository-release-ocai-result.png", + "sha256": "f550b01cc1be63e78e5f2633d11bec50f53b00450a52a38b72cbe0b97e41147b" + }, + { + "path": "docs/images/repository/repository-release-distribution.png", + "sha256": "93f5ff6ec3e75db2b0d1dfc04e637d872f2f6ead81b48941a42a5eb5fff6f94d" + } + ] +} diff --git a/docs/evidence/repository-ui-captures-20261022.json b/docs/evidence/repository-ui-captures-20261022.json new file mode 100644 index 0000000000000000000000000000000000000000..6fd9ce18559dbaa223d5f5687d8a13e8bd7108cb --- /dev/null +++ b/docs/evidence/repository-ui-captures-20261022.json @@ -0,0 +1,77 @@ +{ + "schema_version": 1, + "capture_date": "2026-10-22", + "source_commit": "859648f7914a96d442aad3be8b314af86df36738", + "frontend_source_tree": "72491d599bd8604107eca9cbb4beb91e2a428e93", + "source_kind": "live_browser_current_source_real_backend", + "runtime_frontend": "Local Vite development server using tracked source; not a replacement of the remote production frontend image", + "notes": [ + "Screenshots contain laboratory names, addresses, identifiers and capacities, not deployment defaults. No credential, private key or authorization header is included.", + "List, columns, tasks, task failure, context and analysis result were captured from live pages during documentation refresh. Two read-only analysis sessions were requested for capture; no retry, sync, upload, install, delete or distribution mutation was triggered.", + "The final model-result screenshot was manually scrolled inside the chat result to show Chinese advice. It is advisory output, not proof that its suggested commands ran.", + "Install selector and read-only preview captures were taken earlier on the same frontend source. Preview download/disk change 0 B does not prove a new install.", + "Distribution captures show current UI reading a previously completed two-target record. They do not prove a new distribution in this refresh.", + "Historical images and full 152-item acceptance results keep their original capture/test dates." + ], + "code_baseline_checks": { + "source_commit": "859648f7914a96d442aad3be8b314af86df36738", + "verification_date": "2026-10-22", + "test_command": "npm run test:repository", + "test_files": 16, + "passed_tests": 105, + "scope": "Frontend incremental regression, not 152-item full acceptance", + "rerun_during_docs_only_change": false + }, + "captures": [ + { + "path": "docs/images/repository/repository-ui-20261022-list.png", + "captured_at": "2026-10-22T20:19:04.0856404+08:00", + "sha256": "0d0acaefd8d8bc3759249e01356d5ed15383ce6db5050236a29d6e556120a3d3" + }, + { + "path": "docs/images/repository/repository-ui-20261022-columns.png", + "captured_at": "2026-10-22T20:06:30.0617575+08:00", + "sha256": "3d7bb77469213b44480c6f9fe487f4b493104270d864de30823e295b6ffc5695" + }, + { + "path": "docs/images/repository/repository-ui-20261022-tasks.png", + "captured_at": "2026-10-22T20:06:46.5962637+08:00", + "sha256": "d6c50c9c224f57ead4b20cae2a614024a489cdf61a83f673af550f5833471f64" + }, + { + "path": "docs/images/repository/repository-ui-20261022-task-failure.png", + "captured_at": "2026-10-22T20:07:54.8311794+08:00", + "sha256": "0677b1b10bdacc7e6416d20b403d50c69cfd69a70cdc4d0627e411322cf891fd" + }, + { + "path": "docs/images/repository/repository-ui-20261022-ocai-context.png", + "captured_at": "2026-10-22T20:08:34.3465947+08:00", + "sha256": "1ea0f280684f7c6cf344e178d0c56a06b1405d85dbf8d502fbcbf7ead1dbe4e6" + }, + { + "path": "docs/images/repository/repository-ui-20261022-ocai-result.png", + "captured_at": "2026-10-22T20:17:55.6541502+08:00", + "sha256": "378a07b65b258b6311006ced1ea7dee67411a437af33ee33aa5d7d485c9262b5" + }, + { + "path": "docs/images/repository/repository-ui-20261022-install-nodes.png", + "captured_at": "2026-10-22T19:27:54.6564463+08:00", + "sha256": "2ed247b678615fbe323b3bd2bfbf664e94d2736221e5f5647f948bc3274e608c" + }, + { + "path": "docs/images/repository/repository-ui-20261022-install-preview.png", + "captured_at": "2026-10-22T19:28:19.0181131+08:00", + "sha256": "f826fb6088a09a892cb4993d2832b29bd1cbb41c1a444e44d783e244e02e3f80" + }, + { + "path": "docs/images/repository/repository-ui-20261022-distribution.png", + "captured_at": "2026-10-22T19:40:54.6399426+08:00", + "sha256": "fc6aaa61bbed59f79b59e6315c92cf224f89ce42519d3f233a7630f34e540e9f" + }, + { + "path": "docs/images/repository/repository-ui-20261022-distribution-detail.png", + "captured_at": "2026-10-22T19:27:15.8640898+08:00", + "sha256": "f6411e453004884b30e71ee87b0f0920a2d06f5c0bbe7eb3269b8e8c08dd139e" + } + ] +} diff --git a/docs/images/repository/README.md b/docs/images/repository/README.md new file mode 100644 index 0000000000000000000000000000000000000000..737eb56d21e30642f241d7daa578ce1067a982d4 --- /dev/null +++ b/docs/images/repository/README.md @@ -0,0 +1,41 @@ +# 私有化软件仓库配图 + +本文说明架构配图。当前控制台页面截图和逐步操作见[私有化软件仓库用户手册](../../私有化软件仓库用户手册.md);验收场景与结构化结果见[结构化验收报告](../../私有化软件仓库结构化验收报告.md)。 + +| 图 | 用途 | +| --- | --- | +| [产品与执行全景](repo-architecture.png) | 四个控制台入口、管理协议、管理面、仓库数据面、TMS/Agent、OCAI 与 DNF 客户端 | +| [页面到后端功能映射](repo-feature-map.png) | 将页面操作、真实 RPC、共享执行模块和可见结果逐行对应 | +| [仓库领域关系](repo-er-model.png) | 内容、节点、任务、上传分片、安装授权和签名关系;不是完整物理外键图 | +| [远程发布闭环](repo-publish-flow.png) | Prepare、节点文件切换、Confirm 及异常边界 | +| [任务状态](repo-task-state.png) | 等待、运行、取消、部分成功、失败、lease 重领和人工重试 | + +## 查看与编辑 + +正文引用 PNG,便于 Markdown、邮件与 PDF 显示。每张图同时提供同名 SVG,可放大查看;同名 Draw.io 文件可在 diagrams.net 中打开并逐项编辑。 + +图文和布局的统一维护入口是仓库根目录的 `scripts/render-repository-diagrams.mjs`,没有额外 Node 依赖。修改生成器后重新导出;不要仅修改生成物,否则下一次生成会覆盖手工修改。Draw.io 是可编辑导出,不是第二份独立权威源。 + +在仓库根目录执行(使用项目现有 Node.js 环境): + +```bash +node scripts/render-repository-diagrams.mjs +node scripts/render-repository-diagrams.mjs --check +node --test scripts/tests/repository_diagrams_test.mjs +``` + +生成器更新 SVG、Draw.io、结构化布局 `arch.json`、JSON 兼容的 `spec.yaml` 和 DOT 逻辑关联参考。`--check` 只校验这些文本生成物是否与生成器一致,不重新导出 PNG。DOT 不保留评审版的固定布局。 + +## PNG 导出 + +仅制作图片时需要本机 Chromium/Chrome,以及可用的中文字体,例如 Noto Sans CJK SC;这些不是 OCManager 运行或部署的新增依赖。生成器会在 PATH 中查找浏览器,也可以使用 `OCM_DIAGRAM_BROWSER` 或 `--browser` 指定已安装的浏览器程序。 + +```bash +# 从本机已安装的浏览器导出;字体变化时应重新目视检查排版。 +node scripts/render-repository-diagrams.mjs --png + +# 如浏览器不在 PATH,先将变量设为本机浏览器程序,再执行: +node scripts/render-repository-diagrams.mjs --png --browser "$OCM_DIAGRAM_BROWSER" +``` + +可使用 `--out-dir` 将全部输出写到一个独立预览目录,检查通过后再更新正式图片。生成器不会连接 Repository、修改数据库或操作远端节点。 diff --git a/docs/images/repository/repo-architecture.arch.json b/docs/images/repository/repo-architecture.arch.json new file mode 100644 index 0000000000000000000000000000000000000000..f7ff1f2d2693b1e78c6f23ad4a6cd248313147a9 --- /dev/null +++ b/docs/images/repository/repo-architecture.arch.json @@ -0,0 +1,1144 @@ +{ + "name": "repo-architecture", + "title": "OCManager 私有化软件仓库 · 产品与执行全景", + "subtitle": "四个前端入口统一管理;控制面保存元数据,仓库节点负责内容处理与只读发布。", + "width": 1760, + "height": 1340, + "shapes": [ + { + "id": "ui", + "x": 40, + "y": 124, + "w": 1680, + "h": 210, + "fill": "#F1F6FF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "ui-accent", + "x": 40, + "y": 142, + "w": 4, + "h": 27, + "fill": "#245EC7", + "stroke": "#245EC7", + "r": 1 + }, + { + "id": "ui-repo", + "x": 64, + "y": 184, + "w": 384, + "h": 128, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "ui-task", + "x": 480, + "y": 184, + "w": 384, + "h": 128, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "ui-node", + "x": 896, + "y": 184, + "w": 384, + "h": 128, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "ui-source", + "x": 1312, + "y": 184, + "w": 384, + "h": 128, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "gateway", + "x": 64, + "y": 366, + "w": 1632, + "h": 52, + "fill": "#F1F6FF", + "stroke": "#245EC7", + "r": 8 + }, + { + "id": "control", + "x": 40, + "y": 454, + "w": 1680, + "h": 290, + "fill": "#F1F6FF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "control-accent", + "x": 40, + "y": 472, + "w": 4, + "h": 27, + "fill": "#245EC7", + "stroke": "#245EC7", + "r": 1 + }, + { + "id": "svc-repo", + "x": 64, + "y": 514, + "w": 384, + "h": 138, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "svc-task", + "x": 480, + "y": 514, + "w": 384, + "h": 138, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "svc-node", + "x": 896, + "y": 514, + "w": 384, + "h": 138, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "svc-sync", + "x": 1312, + "y": 514, + "w": 384, + "h": 138, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "mysql", + "x": 64, + "y": 686, + "w": 1632, + "h": 38, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "node", + "x": 40, + "y": 816, + "w": 1088, + "h": 316, + "fill": "#EFF8F3", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "node-accent", + "x": 40, + "y": 834, + "w": 4, + "h": 27, + "fill": "#15765C", + "stroke": "#15765C", + "r": 1 + }, + { + "id": "receive", + "x": 64, + "y": 879, + "w": 320, + "h": 108, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "validate", + "x": 416, + "y": 879, + "w": 320, + "h": 108, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "publish", + "x": 768, + "y": 879, + "w": 336, + "h": 108, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "publisher", + "x": 64, + "y": 1050, + "w": 1040, + "h": 58, + "fill": "#FFFFFF", + "stroke": "#15765C", + "r": 8 + }, + { + "id": "tms", + "x": 1160, + "y": 816, + "w": 560, + "h": 146, + "fill": "#FFF7E9", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "tms-accent", + "x": 1160, + "y": 834, + "w": 4, + "h": 27, + "fill": "#A55F16", + "stroke": "#A55F16", + "r": 1 + }, + { + "id": "ocai", + "x": 1160, + "y": 986, + "w": 560, + "h": 146, + "fill": "#F1F6FF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "ocai-accent", + "x": 1160, + "y": 1004, + "w": 4, + "h": 27, + "fill": "#245EC7", + "stroke": "#245EC7", + "r": 1 + }, + { + "id": "clients", + "x": 40, + "y": 1186, + "w": 1680, + "h": 106, + "fill": "#EFF8F3", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "clients-accent", + "x": 40, + "y": 1204, + "w": 4, + "h": 27, + "fill": "#15765C", + "stroke": "#15765C", + "r": 1 + } + ], + "texts": [ + { + "x": 48, + "y": 55, + "value": "OCManager 私有化软件仓库 · 产品与执行全景", + "size": 34, + "color": "#172B4D", + "weight": 700, + "maxWidth": 1672 + }, + { + "x": 48, + "y": 91, + "value": "四个前端入口统一管理;控制面保存元数据,仓库节点负责内容处理与只读发布。", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 1672 + }, + { + "x": 64, + "y": 163, + "value": "01 控制台:Vue 3 + Pinia + TDesign", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 1632 + }, + { + "x": 84, + "y": 218, + "value": "仓库管理", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 344 + }, + { + "x": 84, + "y": 250, + "value": "列表 / 筛选 / 创建 / 导入导出", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 84, + "y": 277, + "value": "详情:概览、软件包、任务", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 84, + "y": 304, + "value": "节点分发、仓库配置", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 500, + "y": 218, + "value": "任务总览", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 344 + }, + { + "x": 500, + "y": 250, + "value": "四态统计 / 全局筛选", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 500, + "y": 277, + "value": "目标结果 / 逐包结果 / 日志", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 500, + "y": 304, + "value": "失败诊断 / OCAI 建议", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 916, + "y": 218, + "value": "仓库节点", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 344 + }, + { + "x": 916, + "y": 250, + "value": "容量 / 实际占用 / 仓库清单", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 916, + "y": 277, + "value": "节点视角同步 / 最近任务", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 916, + "y": 304, + "value": "Node RPM 安装 / 注册白名单", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 1332, + "y": 218, + "value": "上游源", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 344 + }, + { + "x": 1332, + "y": 250, + "value": "OpenCloudOS 内置来源", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 1332, + "y": 277, + "value": "项目模板 / 版本 / 架构", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 1332, + "y": 304, + "value": "快捷规则与自定义输入", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 218, + "y": 400, + "value": "管理 Nginx → Footstone Repository API · tRPC / JWT / 项目权限", + "size": 25, + "color": "#245EC7", + "weight": 600, + "maxWidth": 1400 + }, + { + "x": 64, + "y": 493, + "value": "02 管理面:沿用 presentation → application → domain → infrastructure", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 1632 + }, + { + "x": 84, + "y": 548, + "value": "仓库与内容编排", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 344 + }, + { + "x": 84, + "y": 580, + "value": "Repository / Content Service", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 84, + "y": 607, + "value": "CRUD、上传、预演、配置生成", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 84, + "y": 634, + "value": "同一入口用于页面和批量 API", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 500, + "y": 548, + "value": "任务与诊断", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 344 + }, + { + "x": 500, + "y": 580, + "value": "Task / Target / Result Item", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 500, + "y": 607, + "value": "日志索引、统一错误、AI 上下文", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 500, + "y": 634, + "value": "状态、重试、取消统一记录", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 916, + "y": 548, + "value": "节点与注册授权", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 344 + }, + { + "x": 916, + "y": 580, + "value": "Node / Usage / Preflight", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 916, + "y": 607, + "value": "安装源、Artifact、Enrollment", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 916, + "y": 634, + "value": "白名单、证书与授权恢复", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 1332, + "y": 548, + "value": "来源与同步调度", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 344 + }, + { + "x": 1332, + "y": 580, + "value": "Upstream Profile / Schedule", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 1332, + "y": 607, + "value": "Include / Exclude / 架构策略", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 1332, + "y": 634, + "value": "幂等调度、限速与下载并发", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 90, + "y": 712, + "value": "MySQL:仓库定义 · Task / Target · Mutation Lock / Lease · Revision / Package 索引 · Node / 授权 · 日志索引", + "size": 20, + "color": "#172B4D", + "weight": 500, + "maxWidth": 1580 + }, + { + "x": 64, + "y": 855, + "value": "03 Repository Node:独立服务与数据目录", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 1040 + }, + { + "x": 84, + "y": 913, + "value": "接收与预检", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 280 + }, + { + "x": 84, + "y": 945, + "value": "上游拉取 / Indexed Chunk", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 280 + }, + { + "x": 84, + "y": 972, + "value": "磁盘、inode、工具与文件系统", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 280 + }, + { + "x": 436, + "y": 913, + "value": "校验与构建", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 280 + }, + { + "x": 436, + "y": 945, + "value": "RPM / GPG / 架构 / SHA-256", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 280 + }, + { + "x": 436, + "y": 972, + "value": "repodata / 可选元数据签名", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 280 + }, + { + "x": 788, + "y": 913, + "value": "不可变发布", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 296 + }, + { + "x": 788, + "y": 945, + "value": "Revision + manifest", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 296 + }, + { + "x": 788, + "y": 972, + "value": "Prepare → Switch → Confirm", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 296 + }, + { + "x": 64, + "y": 1020, + "value": "节点落盘:staging / revisions / quarantine / JSONL.zst;私钥仅在受限 PKI / 签名目录。", + "size": 18, + "color": "#15765C", + "weight": 400, + "maxWidth": 1040 + }, + { + "x": 87, + "y": 1087, + "value": "只读 Nginx:public 挂载 ro;仅 GET / HEAD;稳定地址与 immutable Revision 均保留。", + "size": 20, + "color": "#15765C", + "weight": 600, + "maxWidth": 995 + }, + { + "x": 64, + "y": 771, + "value": "Worker → Footstone:mTLS · Heartbeat / Claim / Prepare / Confirm", + "size": 17, + "color": "#245EC7", + "weight": 500, + "maxWidth": 1656 + }, + { + "x": 754, + "y": 798, + "value": "Probe / Chunk / 公钥 / 结果读取", + "size": 17, + "color": "#245EC7", + "weight": 500, + "maxWidth": 966 + }, + { + "x": 1184, + "y": 855, + "value": "现有 TMS / Agent", + "size": 23, + "color": "#A55F16", + "weight": 700, + "maxWidth": 512 + }, + { + "x": 1184, + "y": 886, + "value": "下发小型命令,保存目标状态与退出码", + "size": 20, + "color": "#172B4D", + "weight": 500, + "maxWidth": 512 + }, + { + "x": 1184, + "y": 918, + "value": "安装 RPM / 安装业务包 / 分发固定快照", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 512 + }, + { + "x": 1455, + "y": 786, + "value": "复用现有远程执行", + "size": 17, + "color": "#A55F16", + "weight": 500, + "maxWidth": 265 + }, + { + "x": 1184, + "y": 1025, + "value": "OCAI:只读分析与建议", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 512 + }, + { + "x": 1184, + "y": 1055, + "value": "API 构造上下文;控制台经网关使用 SSE", + "size": 18, + "color": "#172B4D", + "weight": 500, + "maxWidth": 512 + }, + { + "x": 1184, + "y": 1087, + "value": "原因 / 证据 / 建议;不自动改仓或执行", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 512 + }, + { + "x": 64, + "y": 1225, + "value": "04 使用与反馈:DNF / YUM 客户端、TMS 目标 Agent", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 1632 + }, + { + "x": 64, + "y": 1261, + "value": "客户端从发布地址读取 RPM / repodata;Node RPM 与业务安装绑定不可变版本,任务结果再回到控制台。", + "size": 21, + "color": "#172B4D", + "weight": 400, + "maxWidth": 1620 + }, + { + "x": 636, + "y": 1152, + "value": "客户端请求 GET / HEAD", + "size": 17, + "color": "#15765C", + "weight": 500, + "maxWidth": 1084 + }, + { + "x": 48, + "y": 1314, + "value": "部署边界按角色划分,不强制三台机器;Worker 不直接连接 MySQL;OCAI 不可用时规则诊断仍可使用。", + "size": 17, + "color": "#53657B", + "weight": 400, + "maxWidth": 1664 + } + ], + "edges": [ + { + "id": "ui-gateway-0", + "from": "ui-repo", + "to": "gateway", + "points": [ + [ + 256, + 312 + ], + [ + 256, + 366 + ] + ], + "color": "#245EC7", + "dashed": false, + "reverse": false + }, + { + "id": "ui-gateway-1", + "from": "ui-task", + "to": "gateway", + "points": [ + [ + 672, + 312 + ], + [ + 672, + 366 + ] + ], + "color": "#245EC7", + "dashed": false, + "reverse": false + }, + { + "id": "ui-gateway-2", + "from": "ui-node", + "to": "gateway", + "points": [ + [ + 1088, + 312 + ], + [ + 1088, + 366 + ] + ], + "color": "#245EC7", + "dashed": false, + "reverse": false + }, + { + "id": "ui-gateway-3", + "from": "ui-source", + "to": "gateway", + "points": [ + [ + 1504, + 312 + ], + [ + 1504, + 366 + ] + ], + "color": "#245EC7", + "dashed": false, + "reverse": false + }, + { + "id": "svc-db-0", + "from": "svc-repo", + "to": "mysql", + "points": [ + [ + 256, + 652 + ], + [ + 256, + 686 + ] + ], + "color": "#71839A", + "dashed": false, + "reverse": false + }, + { + "id": "svc-db-1", + "from": "svc-task", + "to": "mysql", + "points": [ + [ + 672, + 652 + ], + [ + 672, + 686 + ] + ], + "color": "#71839A", + "dashed": false, + "reverse": false + }, + { + "id": "svc-db-2", + "from": "svc-node", + "to": "mysql", + "points": [ + [ + 1088, + 652 + ], + [ + 1088, + 686 + ] + ], + "color": "#71839A", + "dashed": false, + "reverse": false + }, + { + "id": "svc-db-3", + "from": "svc-sync", + "to": "mysql", + "points": [ + [ + 1504, + 652 + ], + [ + 1504, + 686 + ] + ], + "color": "#71839A", + "dashed": false, + "reverse": false + }, + { + "id": "gateway-services", + "from": "gateway", + "to": "control", + "points": [ + [ + 880, + 418 + ], + [ + 880, + 454 + ] + ], + "color": "#245EC7", + "dashed": false, + "reverse": false + }, + { + "id": "node-build", + "from": "receive", + "to": "validate", + "points": [ + [ + 384, + 933 + ], + [ + 416, + 933 + ] + ], + "color": "#15765C", + "dashed": false, + "reverse": false + }, + { + "id": "node-publish", + "from": "validate", + "to": "publish", + "points": [ + [ + 736, + 933 + ], + [ + 768, + 933 + ] + ], + "color": "#15765C", + "dashed": false, + "reverse": false + }, + { + "id": "worker-control", + "from": "node", + "to": "control", + "points": [ + [ + 500, + 816 + ], + [ + 500, + 776 + ], + [ + 668, + 776 + ], + [ + 668, + 744 + ] + ], + "color": "#245EC7", + "dashed": false, + "reverse": false + }, + { + "id": "control-node", + "from": "control", + "to": "node", + "points": [ + [ + 1040, + 744 + ], + [ + 1040, + 816 + ] + ], + "color": "#245EC7", + "dashed": false, + "reverse": false + }, + { + "id": "control-tms", + "from": "control", + "to": "tms", + "points": [ + [ + 1440, + 744 + ], + [ + 1440, + 816 + ] + ], + "color": "#A55F16", + "dashed": false, + "reverse": false + }, + { + "id": "gateway-ocai", + "from": "gateway", + "to": "ocai", + "points": [ + [ + 1696, + 392 + ], + [ + 1740, + 392 + ], + [ + 1740, + 1059 + ], + [ + 1720, + 1059 + ] + ], + "color": "#798BA4", + "dashed": true, + "reverse": false + }, + { + "id": "client-request", + "from": "clients", + "to": "publisher", + "points": [ + [ + 620, + 1186 + ], + [ + 620, + 1108 + ] + ], + "color": "#15765C", + "dashed": false, + "reverse": false + } + ] +} diff --git a/docs/images/repository/repo-architecture.dot b/docs/images/repository/repo-architecture.dot new file mode 100644 index 0000000000000000000000000000000000000000..4b9afb6ff16c6c76817d7ca024315827e3b53338 --- /dev/null +++ b/docs/images/repository/repo-architecture.dot @@ -0,0 +1,42 @@ +// Logical edge reference generated by scripts/render-repository-diagrams.mjs. +// The SVG / drawio files retain the reviewed fixed layout. +digraph repo_architecture { + graph [rankdir=LR, fontname="Microsoft YaHei"]; + node [shape=box, fontname="Microsoft YaHei"]; + "ui" [label="01 控制台:Vue 3 + Pinia + TDesign\n仓库管理\n列表 / 筛选 / 创建 / 导入导出\n详情:概览、软件包、任务"]; + "ui-repo" [label="仓库管理\n列表 / 筛选 / 创建 / 导入导出\n详情:概览、软件包、任务\n节点分发、仓库配置"]; + "ui-task" [label="任务总览\n四态统计 / 全局筛选\n目标结果 / 逐包结果 / 日志\n失败诊断 / OCAI 建议"]; + "ui-node" [label="仓库节点\n容量 / 实际占用 / 仓库清单\n节点视角同步 / 最近任务\nNode RPM 安装 / 注册白名单"]; + "ui-source" [label="上游源\nOpenCloudOS 内置来源\n项目模板 / 版本 / 架构\n快捷规则与自定义输入"]; + "gateway" [label="管理 Nginx → Footstone Repository API · tRPC / JWT / 项目权限"]; + "control" [label="02 管理面:沿用 presentation → application → domain → infrastructure\n仓库与内容编排\nRepository / Content Service\nCRUD、上传、预演、配置生成"]; + "svc-repo" [label="仓库与内容编排\nRepository / Content Service\nCRUD、上传、预演、配置生成\n同一入口用于页面和批量 API"]; + "svc-task" [label="任务与诊断\nTask / Target / Result Item\n日志索引、统一错误、AI 上下文\n状态、重试、取消统一记录"]; + "svc-node" [label="节点与注册授权\nNode / Usage / Preflight\n安装源、Artifact、Enrollment\n白名单、证书与授权恢复"]; + "svc-sync" [label="来源与同步调度\nUpstream Profile / Schedule\nInclude / Exclude / 架构策略\n幂等调度、限速与下载并发"]; + "mysql" [label="MySQL:仓库定义 · Task / Target · Mutation Lock / Lease · Revision / Package 索引 · Node / 授权 · 日志索引"]; + "node" [label="03 Repository Node:独立服务与数据目录\n接收与预检\n上游拉取 / Indexed Chunk\n磁盘、inode、工具与文件系统"]; + "receive" [label="接收与预检\n上游拉取 / Indexed Chunk\n磁盘、inode、工具与文件系统"]; + "validate" [label="校验与构建\nRPM / GPG / 架构 / SHA-256\nrepodata / 可选元数据签名"]; + "publish" [label="不可变发布\nRevision + manifest\nPrepare → Switch → Confirm"]; + "publisher" [label="只读 Nginx:public 挂载 ro;仅 GET / HEAD;稳定地址与 immutable Revision 均保留。"]; + "tms" [label="现有 TMS / Agent\n下发小型命令,保存目标状态与退出码\n安装 RPM / 安装业务包 / 分发固定快照"]; + "ocai" [label="OCAI:只读分析与建议\nAPI 构造上下文;控制台经网关使用 SSE\n原因 / 证据 / 建议;不自动改仓或执行"]; + "clients" [label="04 使用与反馈:DNF / YUM 客户端、TMS 目标 Agent\n客户端从发布地址读取 RPM / repodata;Node RPM 与业务安装绑定不可变版本,任务结果再回到控制台。"]; + "ui-repo" -> "gateway" [color="#245EC7"]; + "ui-task" -> "gateway" [color="#245EC7"]; + "ui-node" -> "gateway" [color="#245EC7"]; + "ui-source" -> "gateway" [color="#245EC7"]; + "svc-repo" -> "mysql" [color="#71839A"]; + "svc-task" -> "mysql" [color="#71839A"]; + "svc-node" -> "mysql" [color="#71839A"]; + "svc-sync" -> "mysql" [color="#71839A"]; + "gateway" -> "control" [color="#245EC7"]; + "receive" -> "validate" [color="#15765C"]; + "validate" -> "publish" [color="#15765C"]; + "node" -> "control" [color="#245EC7"]; + "control" -> "node" [color="#245EC7"]; + "control" -> "tms" [color="#A55F16"]; + "gateway" -> "ocai" [color="#798BA4",style=dashed]; + "clients" -> "publisher" [color="#15765C"]; +} diff --git a/docs/images/repository/repo-architecture.drawio b/docs/images/repository/repo-architecture.drawio new file mode 100644 index 0000000000000000000000000000000000000000..a461f7d991a855bdd7afcbf8762831859ada26ac --- /dev/null +++ b/docs/images/repository/repo-architecture.drawio @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/repository/repo-architecture.png b/docs/images/repository/repo-architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..90737415c7ebe757739971066b8895929e4ec449 Binary files /dev/null and b/docs/images/repository/repo-architecture.png differ diff --git a/docs/images/repository/repo-architecture.spec.yaml b/docs/images/repository/repo-architecture.spec.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f7ff1f2d2693b1e78c6f23ad4a6cd248313147a9 --- /dev/null +++ b/docs/images/repository/repo-architecture.spec.yaml @@ -0,0 +1,1144 @@ +{ + "name": "repo-architecture", + "title": "OCManager 私有化软件仓库 · 产品与执行全景", + "subtitle": "四个前端入口统一管理;控制面保存元数据,仓库节点负责内容处理与只读发布。", + "width": 1760, + "height": 1340, + "shapes": [ + { + "id": "ui", + "x": 40, + "y": 124, + "w": 1680, + "h": 210, + "fill": "#F1F6FF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "ui-accent", + "x": 40, + "y": 142, + "w": 4, + "h": 27, + "fill": "#245EC7", + "stroke": "#245EC7", + "r": 1 + }, + { + "id": "ui-repo", + "x": 64, + "y": 184, + "w": 384, + "h": 128, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "ui-task", + "x": 480, + "y": 184, + "w": 384, + "h": 128, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "ui-node", + "x": 896, + "y": 184, + "w": 384, + "h": 128, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "ui-source", + "x": 1312, + "y": 184, + "w": 384, + "h": 128, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "gateway", + "x": 64, + "y": 366, + "w": 1632, + "h": 52, + "fill": "#F1F6FF", + "stroke": "#245EC7", + "r": 8 + }, + { + "id": "control", + "x": 40, + "y": 454, + "w": 1680, + "h": 290, + "fill": "#F1F6FF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "control-accent", + "x": 40, + "y": 472, + "w": 4, + "h": 27, + "fill": "#245EC7", + "stroke": "#245EC7", + "r": 1 + }, + { + "id": "svc-repo", + "x": 64, + "y": 514, + "w": 384, + "h": 138, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "svc-task", + "x": 480, + "y": 514, + "w": 384, + "h": 138, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "svc-node", + "x": 896, + "y": 514, + "w": 384, + "h": 138, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "svc-sync", + "x": 1312, + "y": 514, + "w": 384, + "h": 138, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "mysql", + "x": 64, + "y": 686, + "w": 1632, + "h": 38, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "node", + "x": 40, + "y": 816, + "w": 1088, + "h": 316, + "fill": "#EFF8F3", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "node-accent", + "x": 40, + "y": 834, + "w": 4, + "h": 27, + "fill": "#15765C", + "stroke": "#15765C", + "r": 1 + }, + { + "id": "receive", + "x": 64, + "y": 879, + "w": 320, + "h": 108, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "validate", + "x": 416, + "y": 879, + "w": 320, + "h": 108, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "publish", + "x": 768, + "y": 879, + "w": 336, + "h": 108, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "publisher", + "x": 64, + "y": 1050, + "w": 1040, + "h": 58, + "fill": "#FFFFFF", + "stroke": "#15765C", + "r": 8 + }, + { + "id": "tms", + "x": 1160, + "y": 816, + "w": 560, + "h": 146, + "fill": "#FFF7E9", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "tms-accent", + "x": 1160, + "y": 834, + "w": 4, + "h": 27, + "fill": "#A55F16", + "stroke": "#A55F16", + "r": 1 + }, + { + "id": "ocai", + "x": 1160, + "y": 986, + "w": 560, + "h": 146, + "fill": "#F1F6FF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "ocai-accent", + "x": 1160, + "y": 1004, + "w": 4, + "h": 27, + "fill": "#245EC7", + "stroke": "#245EC7", + "r": 1 + }, + { + "id": "clients", + "x": 40, + "y": 1186, + "w": 1680, + "h": 106, + "fill": "#EFF8F3", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "clients-accent", + "x": 40, + "y": 1204, + "w": 4, + "h": 27, + "fill": "#15765C", + "stroke": "#15765C", + "r": 1 + } + ], + "texts": [ + { + "x": 48, + "y": 55, + "value": "OCManager 私有化软件仓库 · 产品与执行全景", + "size": 34, + "color": "#172B4D", + "weight": 700, + "maxWidth": 1672 + }, + { + "x": 48, + "y": 91, + "value": "四个前端入口统一管理;控制面保存元数据,仓库节点负责内容处理与只读发布。", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 1672 + }, + { + "x": 64, + "y": 163, + "value": "01 控制台:Vue 3 + Pinia + TDesign", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 1632 + }, + { + "x": 84, + "y": 218, + "value": "仓库管理", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 344 + }, + { + "x": 84, + "y": 250, + "value": "列表 / 筛选 / 创建 / 导入导出", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 84, + "y": 277, + "value": "详情:概览、软件包、任务", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 84, + "y": 304, + "value": "节点分发、仓库配置", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 500, + "y": 218, + "value": "任务总览", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 344 + }, + { + "x": 500, + "y": 250, + "value": "四态统计 / 全局筛选", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 500, + "y": 277, + "value": "目标结果 / 逐包结果 / 日志", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 500, + "y": 304, + "value": "失败诊断 / OCAI 建议", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 916, + "y": 218, + "value": "仓库节点", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 344 + }, + { + "x": 916, + "y": 250, + "value": "容量 / 实际占用 / 仓库清单", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 916, + "y": 277, + "value": "节点视角同步 / 最近任务", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 916, + "y": 304, + "value": "Node RPM 安装 / 注册白名单", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 1332, + "y": 218, + "value": "上游源", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 344 + }, + { + "x": 1332, + "y": 250, + "value": "OpenCloudOS 内置来源", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 1332, + "y": 277, + "value": "项目模板 / 版本 / 架构", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 1332, + "y": 304, + "value": "快捷规则与自定义输入", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 218, + "y": 400, + "value": "管理 Nginx → Footstone Repository API · tRPC / JWT / 项目权限", + "size": 25, + "color": "#245EC7", + "weight": 600, + "maxWidth": 1400 + }, + { + "x": 64, + "y": 493, + "value": "02 管理面:沿用 presentation → application → domain → infrastructure", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 1632 + }, + { + "x": 84, + "y": 548, + "value": "仓库与内容编排", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 344 + }, + { + "x": 84, + "y": 580, + "value": "Repository / Content Service", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 84, + "y": 607, + "value": "CRUD、上传、预演、配置生成", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 84, + "y": 634, + "value": "同一入口用于页面和批量 API", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 500, + "y": 548, + "value": "任务与诊断", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 344 + }, + { + "x": 500, + "y": 580, + "value": "Task / Target / Result Item", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 500, + "y": 607, + "value": "日志索引、统一错误、AI 上下文", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 500, + "y": 634, + "value": "状态、重试、取消统一记录", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 916, + "y": 548, + "value": "节点与注册授权", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 344 + }, + { + "x": 916, + "y": 580, + "value": "Node / Usage / Preflight", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 916, + "y": 607, + "value": "安装源、Artifact、Enrollment", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 916, + "y": 634, + "value": "白名单、证书与授权恢复", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 1332, + "y": 548, + "value": "来源与同步调度", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 344 + }, + { + "x": 1332, + "y": 580, + "value": "Upstream Profile / Schedule", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 1332, + "y": 607, + "value": "Include / Exclude / 架构策略", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 1332, + "y": 634, + "value": "幂等调度、限速与下载并发", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 344 + }, + { + "x": 90, + "y": 712, + "value": "MySQL:仓库定义 · Task / Target · Mutation Lock / Lease · Revision / Package 索引 · Node / 授权 · 日志索引", + "size": 20, + "color": "#172B4D", + "weight": 500, + "maxWidth": 1580 + }, + { + "x": 64, + "y": 855, + "value": "03 Repository Node:独立服务与数据目录", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 1040 + }, + { + "x": 84, + "y": 913, + "value": "接收与预检", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 280 + }, + { + "x": 84, + "y": 945, + "value": "上游拉取 / Indexed Chunk", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 280 + }, + { + "x": 84, + "y": 972, + "value": "磁盘、inode、工具与文件系统", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 280 + }, + { + "x": 436, + "y": 913, + "value": "校验与构建", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 280 + }, + { + "x": 436, + "y": 945, + "value": "RPM / GPG / 架构 / SHA-256", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 280 + }, + { + "x": 436, + "y": 972, + "value": "repodata / 可选元数据签名", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 280 + }, + { + "x": 788, + "y": 913, + "value": "不可变发布", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 296 + }, + { + "x": 788, + "y": 945, + "value": "Revision + manifest", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 296 + }, + { + "x": 788, + "y": 972, + "value": "Prepare → Switch → Confirm", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 296 + }, + { + "x": 64, + "y": 1020, + "value": "节点落盘:staging / revisions / quarantine / JSONL.zst;私钥仅在受限 PKI / 签名目录。", + "size": 18, + "color": "#15765C", + "weight": 400, + "maxWidth": 1040 + }, + { + "x": 87, + "y": 1087, + "value": "只读 Nginx:public 挂载 ro;仅 GET / HEAD;稳定地址与 immutable Revision 均保留。", + "size": 20, + "color": "#15765C", + "weight": 600, + "maxWidth": 995 + }, + { + "x": 64, + "y": 771, + "value": "Worker → Footstone:mTLS · Heartbeat / Claim / Prepare / Confirm", + "size": 17, + "color": "#245EC7", + "weight": 500, + "maxWidth": 1656 + }, + { + "x": 754, + "y": 798, + "value": "Probe / Chunk / 公钥 / 结果读取", + "size": 17, + "color": "#245EC7", + "weight": 500, + "maxWidth": 966 + }, + { + "x": 1184, + "y": 855, + "value": "现有 TMS / Agent", + "size": 23, + "color": "#A55F16", + "weight": 700, + "maxWidth": 512 + }, + { + "x": 1184, + "y": 886, + "value": "下发小型命令,保存目标状态与退出码", + "size": 20, + "color": "#172B4D", + "weight": 500, + "maxWidth": 512 + }, + { + "x": 1184, + "y": 918, + "value": "安装 RPM / 安装业务包 / 分发固定快照", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 512 + }, + { + "x": 1455, + "y": 786, + "value": "复用现有远程执行", + "size": 17, + "color": "#A55F16", + "weight": 500, + "maxWidth": 265 + }, + { + "x": 1184, + "y": 1025, + "value": "OCAI:只读分析与建议", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 512 + }, + { + "x": 1184, + "y": 1055, + "value": "API 构造上下文;控制台经网关使用 SSE", + "size": 18, + "color": "#172B4D", + "weight": 500, + "maxWidth": 512 + }, + { + "x": 1184, + "y": 1087, + "value": "原因 / 证据 / 建议;不自动改仓或执行", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 512 + }, + { + "x": 64, + "y": 1225, + "value": "04 使用与反馈:DNF / YUM 客户端、TMS 目标 Agent", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 1632 + }, + { + "x": 64, + "y": 1261, + "value": "客户端从发布地址读取 RPM / repodata;Node RPM 与业务安装绑定不可变版本,任务结果再回到控制台。", + "size": 21, + "color": "#172B4D", + "weight": 400, + "maxWidth": 1620 + }, + { + "x": 636, + "y": 1152, + "value": "客户端请求 GET / HEAD", + "size": 17, + "color": "#15765C", + "weight": 500, + "maxWidth": 1084 + }, + { + "x": 48, + "y": 1314, + "value": "部署边界按角色划分,不强制三台机器;Worker 不直接连接 MySQL;OCAI 不可用时规则诊断仍可使用。", + "size": 17, + "color": "#53657B", + "weight": 400, + "maxWidth": 1664 + } + ], + "edges": [ + { + "id": "ui-gateway-0", + "from": "ui-repo", + "to": "gateway", + "points": [ + [ + 256, + 312 + ], + [ + 256, + 366 + ] + ], + "color": "#245EC7", + "dashed": false, + "reverse": false + }, + { + "id": "ui-gateway-1", + "from": "ui-task", + "to": "gateway", + "points": [ + [ + 672, + 312 + ], + [ + 672, + 366 + ] + ], + "color": "#245EC7", + "dashed": false, + "reverse": false + }, + { + "id": "ui-gateway-2", + "from": "ui-node", + "to": "gateway", + "points": [ + [ + 1088, + 312 + ], + [ + 1088, + 366 + ] + ], + "color": "#245EC7", + "dashed": false, + "reverse": false + }, + { + "id": "ui-gateway-3", + "from": "ui-source", + "to": "gateway", + "points": [ + [ + 1504, + 312 + ], + [ + 1504, + 366 + ] + ], + "color": "#245EC7", + "dashed": false, + "reverse": false + }, + { + "id": "svc-db-0", + "from": "svc-repo", + "to": "mysql", + "points": [ + [ + 256, + 652 + ], + [ + 256, + 686 + ] + ], + "color": "#71839A", + "dashed": false, + "reverse": false + }, + { + "id": "svc-db-1", + "from": "svc-task", + "to": "mysql", + "points": [ + [ + 672, + 652 + ], + [ + 672, + 686 + ] + ], + "color": "#71839A", + "dashed": false, + "reverse": false + }, + { + "id": "svc-db-2", + "from": "svc-node", + "to": "mysql", + "points": [ + [ + 1088, + 652 + ], + [ + 1088, + 686 + ] + ], + "color": "#71839A", + "dashed": false, + "reverse": false + }, + { + "id": "svc-db-3", + "from": "svc-sync", + "to": "mysql", + "points": [ + [ + 1504, + 652 + ], + [ + 1504, + 686 + ] + ], + "color": "#71839A", + "dashed": false, + "reverse": false + }, + { + "id": "gateway-services", + "from": "gateway", + "to": "control", + "points": [ + [ + 880, + 418 + ], + [ + 880, + 454 + ] + ], + "color": "#245EC7", + "dashed": false, + "reverse": false + }, + { + "id": "node-build", + "from": "receive", + "to": "validate", + "points": [ + [ + 384, + 933 + ], + [ + 416, + 933 + ] + ], + "color": "#15765C", + "dashed": false, + "reverse": false + }, + { + "id": "node-publish", + "from": "validate", + "to": "publish", + "points": [ + [ + 736, + 933 + ], + [ + 768, + 933 + ] + ], + "color": "#15765C", + "dashed": false, + "reverse": false + }, + { + "id": "worker-control", + "from": "node", + "to": "control", + "points": [ + [ + 500, + 816 + ], + [ + 500, + 776 + ], + [ + 668, + 776 + ], + [ + 668, + 744 + ] + ], + "color": "#245EC7", + "dashed": false, + "reverse": false + }, + { + "id": "control-node", + "from": "control", + "to": "node", + "points": [ + [ + 1040, + 744 + ], + [ + 1040, + 816 + ] + ], + "color": "#245EC7", + "dashed": false, + "reverse": false + }, + { + "id": "control-tms", + "from": "control", + "to": "tms", + "points": [ + [ + 1440, + 744 + ], + [ + 1440, + 816 + ] + ], + "color": "#A55F16", + "dashed": false, + "reverse": false + }, + { + "id": "gateway-ocai", + "from": "gateway", + "to": "ocai", + "points": [ + [ + 1696, + 392 + ], + [ + 1740, + 392 + ], + [ + 1740, + 1059 + ], + [ + 1720, + 1059 + ] + ], + "color": "#798BA4", + "dashed": true, + "reverse": false + }, + { + "id": "client-request", + "from": "clients", + "to": "publisher", + "points": [ + [ + 620, + 1186 + ], + [ + 620, + 1108 + ] + ], + "color": "#15765C", + "dashed": false, + "reverse": false + } + ] +} diff --git a/docs/images/repository/repo-architecture.svg b/docs/images/repository/repo-architecture.svg new file mode 100644 index 0000000000000000000000000000000000000000..11adf8325d3f3e553d08c5794a34f98a630c7d81 --- /dev/null +++ b/docs/images/repository/repo-architecture.svg @@ -0,0 +1,111 @@ + + +OCManager 私有化软件仓库 · 产品与执行全景四个前端入口统一管理;控制面保存元数据,仓库节点负责内容处理与只读发布。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +OCManager 私有化软件仓库 · 产品与执行全景 +四个前端入口统一管理;控制面保存元数据,仓库节点负责内容处理与只读发布。 +01 控制台:Vue 3 + Pinia + TDesign +仓库管理 +列表 / 筛选 / 创建 / 导入导出 +详情:概览、软件包、任务 +节点分发、仓库配置 +任务总览 +四态统计 / 全局筛选 +目标结果 / 逐包结果 / 日志 +失败诊断 / OCAI 建议 +仓库节点 +容量 / 实际占用 / 仓库清单 +节点视角同步 / 最近任务 +Node RPM 安装 / 注册白名单 +上游源 +OpenCloudOS 内置来源 +项目模板 / 版本 / 架构 +快捷规则与自定义输入 +管理 Nginx → Footstone Repository API · tRPC / JWT / 项目权限 +02 管理面:沿用 presentation → application → domain → infrastructure +仓库与内容编排 +Repository / Content Service +CRUD、上传、预演、配置生成 +同一入口用于页面和批量 API +任务与诊断 +Task / Target / Result Item +日志索引、统一错误、AI 上下文 +状态、重试、取消统一记录 +节点与注册授权 +Node / Usage / Preflight +安装源、Artifact、Enrollment +白名单、证书与授权恢复 +来源与同步调度 +Upstream Profile / Schedule +Include / Exclude / 架构策略 +幂等调度、限速与下载并发 +MySQL:仓库定义 · Task / Target · Mutation Lock / Lease · Revision / Package 索引 · Node / 授权 · 日志索引 +03 Repository Node:独立服务与数据目录 +接收与预检 +上游拉取 / Indexed Chunk +磁盘、inode、工具与文件系统 +校验与构建 +RPM / GPG / 架构 / SHA-256 +repodata / 可选元数据签名 +不可变发布 +Revision + manifest +Prepare → Switch → Confirm +节点落盘:staging / revisions / quarantine / JSONL.zst;私钥仅在受限 PKI / 签名目录。 +只读 Nginx:public 挂载 ro;仅 GET / HEAD;稳定地址与 immutable Revision 均保留。 +Worker → Footstone:mTLS · Heartbeat / Claim / Prepare / Confirm +Probe / Chunk / 公钥 / 结果读取 +现有 TMS / Agent +下发小型命令,保存目标状态与退出码 +安装 RPM / 安装业务包 / 分发固定快照 +复用现有远程执行 +OCAI:只读分析与建议 +API 构造上下文;控制台经网关使用 SSE +原因 / 证据 / 建议;不自动改仓或执行 +04 使用与反馈:DNF / YUM 客户端、TMS 目标 Agent +客户端从发布地址读取 RPM / repodata;Node RPM 与业务安装绑定不可变版本,任务结果再回到控制台。 +客户端请求 GET / HEAD +部署边界按角色划分,不强制三台机器;Worker 不直接连接 MySQL;OCAI 不可用时规则诊断仍可使用。 + diff --git a/docs/images/repository/repo-er-model.arch.json b/docs/images/repository/repo-er-model.arch.json new file mode 100644 index 0000000000000000000000000000000000000000..90abc17e49c62fc1c696c863153ab65bfb7899cf --- /dev/null +++ b/docs/images/repository/repo-er-model.arch.json @@ -0,0 +1,904 @@ +{ + "name": "repo-er-model", + "title": "仓库领域关系 · 定义、内容、任务与节点身份", + "subtitle": "按领域聚合展示主要关联;并非全部物理外键。精确字段、约束与默认值见 v5—v18 Migration。", + "width": 1760, + "height": 1450, + "shapes": [ + { + "id": "nodes", + "x": 64, + "y": 156, + "w": 480, + "h": 154, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "repository", + "x": 640, + "y": 156, + "w": 480, + "h": 154, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "profiles", + "x": 1216, + "y": 156, + "w": 480, + "h": 154, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "tasks", + "x": 64, + "y": 414, + "w": 480, + "h": 278, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "contents", + "x": 640, + "y": 414, + "w": 480, + "h": 278, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "enrollment", + "x": 1216, + "y": 414, + "w": 480, + "h": 278, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "workflow", + "x": 64, + "y": 808, + "w": 480, + "h": 258, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "signing", + "x": 640, + "y": 808, + "w": 480, + "h": 258, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "usage", + "x": 1216, + "y": 808, + "w": 480, + "h": 258, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "truth", + "x": 40, + "y": 1164, + "w": 1680, + "h": 216, + "fill": "#F7F9FC", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "truth-accent", + "x": 40, + "y": 1182, + "w": 4, + "h": 27, + "fill": "#172B4D", + "stroke": "#172B4D", + "r": 1 + } + ], + "texts": [ + { + "x": 48, + "y": 55, + "value": "仓库领域关系 · 定义、内容、任务与节点身份", + "size": 34, + "color": "#172B4D", + "weight": 700, + "maxWidth": 1672 + }, + { + "x": 48, + "y": 91, + "value": "按领域聚合展示主要关联;并非全部物理外键。精确字段、约束与默认值见 v5—v18 Migration。", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 1672 + }, + { + "x": 84, + "y": 190, + "value": "仓库节点", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 440 + }, + { + "x": 84, + "y": 222, + "value": "repository_storage_nodes", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 84, + "y": 249, + "value": "控制 / 发布地址、心跳与文件系统", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 84, + "y": 276, + "value": "Storage Node 身份独立于 TMS Agent", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 660, + "y": 190, + "value": "仓库定义", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 440 + }, + { + "x": 660, + "y": 222, + "value": "software_repositories", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 660, + "y": 249, + "value": "项目 / 产品版本 / 架构 / purpose", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 660, + "y": 276, + "value": "active_revision_id / primary_node", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 190, + "value": "上游来源模板", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 222, + "value": "repository_upstream_profiles", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 249, + "value": "版本 / 架构 / URL 模板 / 默认规则", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 276, + "value": "解析配置,不静默改动已有仓库", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 550, + "y": 215, + "value": "1 → 0..N", + "size": 17, + "color": "#15765C", + "weight": 500, + "maxWidth": 1170 + }, + { + "x": 1128, + "y": 215, + "value": "配置快照", + "size": 17, + "color": "#245EC7", + "weight": 500, + "maxWidth": 592 + }, + { + "x": 84, + "y": 448, + "value": "统一业务任务与执行结果", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 440 + }, + { + "x": 84, + "y": 480, + "value": "repository_tasks", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 84, + "y": 507, + "value": "Task → Targets:0..N", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 84, + "y": 534, + "value": "Task → Result Items:0..N", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 84, + "y": 561, + "value": "Task → Log Index:0..1", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 84, + "y": 588, + "value": "Task → OCAI Analyses:0..N", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 84, + "y": 615, + "value": "lease / fencing / 同仓库 Mutation Lock", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 660, + "y": 448, + "value": "内容会话与不可变版本", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 440 + }, + { + "x": 660, + "y": 480, + "value": "Repository → Revisions:0..N", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 660, + "y": 507, + "value": "Revision → Packages:0..N", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 660, + "y": 534, + "value": "Repository → Upload Sessions", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 660, + "y": 561, + "value": "Upload → Indexed Chunks:0..N", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 660, + "y": 588, + "value": "Quarantine:拒绝包与原因", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 660, + "y": 615, + "value": "RPM 实体 / repodata 留在仓库节点", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 448, + "value": "Node RPM 与注册身份", + "size": 23, + "color": "#A55F16", + "weight": 700, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 480, + "value": "Install Source → 普通 NODE_RPM 仓", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 507, + "value": "Artifact → Package + Revision + SHA", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 534, + "value": "Allowlist(project, Agent) → Enrollment", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 561, + "value": "Enrollment → Token / 授权与 CSR 状态", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 588, + "value": "Certificate 表:唯一公开证书权威", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 615, + "value": "私钥只保存在节点本地", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 366, + "y": 348, + "value": "repository_id", + "size": 17, + "color": "#245EC7", + "weight": 500, + "maxWidth": 1354 + }, + { + "x": 900, + "y": 342, + "value": "active / history", + "size": 17, + "color": "#15765C", + "weight": 500, + "maxWidth": 820 + }, + { + "x": 1186, + "y": 348, + "value": "受保护的标准安装源", + "size": 17, + "color": "#A55F16", + "weight": 500, + "maxWidth": 534 + }, + { + "x": 84, + "y": 842, + "value": "调度、预演与业务作业", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 440 + }, + { + "x": 84, + "y": 874, + "value": "Repository → Schedule:0..1", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 84, + "y": 901, + "value": "Schedule → Schedule Runs → Task", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 84, + "y": 928, + "value": "Preview:Digest / 版本 / 有效期", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 84, + "y": 955, + "value": "Package Install → 同一 Task / Targets", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 84, + "y": 982, + "value": "Inventory Import Job → 同一 Task", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 84, + "y": 1009, + "value": "Chunk 表是接收分片的事实来源", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 660, + "y": 842, + "value": "包校验与元数据签名", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 440 + }, + { + "x": 660, + "y": 874, + "value": "Repository → GPG 公钥元数据", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 660, + "y": 901, + "value": "Node → Metadata Key Inventory", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 660, + "y": 928, + "value": "Repository 仅引用 key_ref", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 660, + "y": 955, + "value": "Revision 固化签名结果与公钥 URL", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 660, + "y": 982, + "value": "旧 Revision 不因换 Key 被重签", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 660, + "y": 1009, + "value": "不保存 / 导出 GPG 私钥", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 842, + "value": "节点占用与安装观测", + "size": 23, + "color": "#A55F16", + "weight": 700, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 874, + "value": "Node → Repository Usages:0..N", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 901, + "value": "logical:active RPM 内容大小", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 928, + "value": "physical:可归属的磁盘分配空间", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 955, + "value": "Node 总量 / 可用量来自 statfs", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 982, + "value": "Agent 能力、架构和 Node RPM 版本", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 1009, + "value": "Target 分别保存退出码与失败原因", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 324, + "y": 756, + "value": "Run / Job 关联 Task", + "size": 17, + "color": "#245EC7", + "weight": 500, + "maxWidth": 1396 + }, + { + "x": 900, + "y": 756, + "value": "签名随 Revision 固化", + "size": 17, + "color": "#15765C", + "weight": 500, + "maxWidth": 820 + }, + { + "x": 1470, + "y": 756, + "value": "上线后心跳与目标结果", + "size": 17, + "color": "#A55F16", + "weight": 500, + "maxWidth": 250 + }, + { + "x": 64, + "y": 1203, + "value": "三条权威边界", + "size": 23, + "color": "#172B4D", + "weight": 700, + "maxWidth": 1632 + }, + { + "x": 64, + "y": 1250, + "value": "① 数据库保存定义与索引;RPM、repodata、隔离文件、原始压缩日志保存于 Node。", + "size": 22, + "color": "#172B4D", + "weight": 500, + "maxWidth": 1600 + }, + { + "x": 64, + "y": 1295, + "value": "② Task 管整体状态,Target 管节点结果,Result Item 管逐包结果;证书不设第二套权威模型。", + "size": 22, + "color": "#172B4D", + "weight": 500, + "maxWidth": 1600 + }, + { + "x": 64, + "y": 1340, + "value": "③ Inventory 导出的是定义,不是内容备份;不包含私钥、Bootstrap Token、TMS / OCAI 凭据。", + "size": 22, + "color": "#172B4D", + "weight": 500, + "maxWidth": 1600 + }, + { + "x": 48, + "y": 1424, + "value": "图中“报告”属于结果与导出,不表示另建 validation_reports 表;完整表名、索引和兼容升级规则见数据库设计正文。", + "size": 17, + "color": "#53657B", + "weight": 400, + "maxWidth": 1664 + } + ], + "edges": [ + { + "id": "node-repos", + "from": "nodes", + "to": "repository", + "points": [ + [ + 544, + 232 + ], + [ + 640, + 232 + ] + ], + "color": "#15765C", + "dashed": false, + "reverse": false + }, + { + "id": "template-repo", + "from": "profiles", + "to": "repository", + "points": [ + [ + 1216, + 232 + ], + [ + 1120, + 232 + ] + ], + "color": "#245EC7", + "dashed": true, + "reverse": false + }, + { + "id": "repo-task", + "from": "repository", + "to": "tasks", + "points": [ + [ + 760, + 310 + ], + [ + 760, + 360 + ], + [ + 304, + 360 + ], + [ + 304, + 414 + ] + ], + "color": "#245EC7", + "dashed": false, + "reverse": false + }, + { + "id": "repo-revision", + "from": "repository", + "to": "contents", + "points": [ + [ + 880, + 310 + ], + [ + 880, + 414 + ] + ], + "color": "#15765C", + "dashed": false, + "reverse": false + }, + { + "id": "repo-artifact", + "from": "repository", + "to": "enrollment", + "points": [ + [ + 1000, + 310 + ], + [ + 1000, + 360 + ], + [ + 1456, + 360 + ], + [ + 1456, + 414 + ] + ], + "color": "#A55F16", + "dashed": false, + "reverse": false + }, + { + "id": "run-task", + "from": "workflow", + "to": "tasks", + "points": [ + [ + 304, + 808 + ], + [ + 304, + 692 + ] + ], + "color": "#245EC7", + "dashed": false, + "reverse": false + }, + { + "id": "revision-signature", + "from": "contents", + "to": "signing", + "points": [ + [ + 880, + 692 + ], + [ + 880, + 808 + ] + ], + "color": "#15765C", + "dashed": false, + "reverse": false + }, + { + "id": "enroll-node", + "from": "enrollment", + "to": "usage", + "points": [ + [ + 1456, + 692 + ], + [ + 1456, + 808 + ] + ], + "color": "#A55F16", + "dashed": false, + "reverse": false + } + ] +} diff --git a/docs/images/repository/repo-er-model.dot b/docs/images/repository/repo-er-model.dot new file mode 100644 index 0000000000000000000000000000000000000000..37e7c459fdee65f8113e0901403602c7e27020ce --- /dev/null +++ b/docs/images/repository/repo-er-model.dot @@ -0,0 +1,24 @@ +// Logical edge reference generated by scripts/render-repository-diagrams.mjs. +// The SVG / drawio files retain the reviewed fixed layout. +digraph repo_er_model { + graph [rankdir=LR, fontname="Microsoft YaHei"]; + node [shape=box, fontname="Microsoft YaHei"]; + "nodes" [label="仓库节点\nrepository_storage_nodes\n控制 / 发布地址、心跳与文件系统\nStorage Node 身份独立于 TMS Agent"]; + "repository" [label="仓库定义\nsoftware_repositories\n项目 / 产品版本 / 架构 / purpose\nactive_revision_id / primary_node"]; + "profiles" [label="上游来源模板\nrepository_upstream_profiles\n版本 / 架构 / URL 模板 / 默认规则\n解析配置,不静默改动已有仓库"]; + "tasks" [label="统一业务任务与执行结果\nrepository_tasks\nTask → Targets:0..N\nTask → Result Items:0..N"]; + "contents" [label="内容会话与不可变版本\nRepository → Revisions:0..N\nRevision → Packages:0..N\nRepository → Upload Sessions"]; + "enrollment" [label="Node RPM 与注册身份\nInstall Source → 普通 NODE_RPM 仓\nArtifact → Package + Revision + SHA\nAllowlist(project, Agent) → Enrollment"]; + "workflow" [label="调度、预演与业务作业\nRepository → Schedule:0..1\nSchedule → Schedule Runs → Task\nPreview:Digest / 版本 / 有效期"]; + "signing" [label="包校验与元数据签名\nRepository → GPG 公钥元数据\nNode → Metadata Key Inventory\nRepository 仅引用 key_ref"]; + "usage" [label="节点占用与安装观测\nNode → Repository Usages:0..N\nlogical:active RPM 内容大小\nphysical:可归属的磁盘分配空间"]; + "truth" [label="三条权威边界\n① 数据库保存定义与索引;RPM、repodata、隔离文件、原始压缩日志保存于 Node。\n② Task 管整体状态,Target 管节点结果,Result Item 管逐包结果;证书不设第二套权威模型。\n③ Inventory 导出的是定义,不是内容备份;不包含私钥、Bootstrap Token、TMS / OCAI 凭据。"]; + "nodes" -> "repository" [color="#15765C"]; + "profiles" -> "repository" [color="#245EC7",style=dashed]; + "repository" -> "tasks" [color="#245EC7"]; + "repository" -> "contents" [color="#15765C"]; + "repository" -> "enrollment" [color="#A55F16"]; + "workflow" -> "tasks" [color="#245EC7"]; + "contents" -> "signing" [color="#15765C"]; + "enrollment" -> "usage" [color="#A55F16"]; +} diff --git a/docs/images/repository/repo-er-model.drawio b/docs/images/repository/repo-er-model.drawio new file mode 100644 index 0000000000000000000000000000000000000000..3cb0f5d52795e4c65c573538a7157b9249045d9d --- /dev/null +++ b/docs/images/repository/repo-er-model.drawio @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/repository/repo-er-model.png b/docs/images/repository/repo-er-model.png new file mode 100644 index 0000000000000000000000000000000000000000..bcda1e0d40756aba2bffd3187e280ca869a98800 Binary files /dev/null and b/docs/images/repository/repo-er-model.png differ diff --git a/docs/images/repository/repo-er-model.spec.yaml b/docs/images/repository/repo-er-model.spec.yaml new file mode 100644 index 0000000000000000000000000000000000000000..90abc17e49c62fc1c696c863153ab65bfb7899cf --- /dev/null +++ b/docs/images/repository/repo-er-model.spec.yaml @@ -0,0 +1,904 @@ +{ + "name": "repo-er-model", + "title": "仓库领域关系 · 定义、内容、任务与节点身份", + "subtitle": "按领域聚合展示主要关联;并非全部物理外键。精确字段、约束与默认值见 v5—v18 Migration。", + "width": 1760, + "height": 1450, + "shapes": [ + { + "id": "nodes", + "x": 64, + "y": 156, + "w": 480, + "h": 154, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "repository", + "x": 640, + "y": 156, + "w": 480, + "h": 154, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "profiles", + "x": 1216, + "y": 156, + "w": 480, + "h": 154, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "tasks", + "x": 64, + "y": 414, + "w": 480, + "h": 278, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "contents", + "x": 640, + "y": 414, + "w": 480, + "h": 278, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "enrollment", + "x": 1216, + "y": 414, + "w": 480, + "h": 278, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "workflow", + "x": 64, + "y": 808, + "w": 480, + "h": 258, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "signing", + "x": 640, + "y": 808, + "w": 480, + "h": 258, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "usage", + "x": 1216, + "y": 808, + "w": 480, + "h": 258, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "truth", + "x": 40, + "y": 1164, + "w": 1680, + "h": 216, + "fill": "#F7F9FC", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "truth-accent", + "x": 40, + "y": 1182, + "w": 4, + "h": 27, + "fill": "#172B4D", + "stroke": "#172B4D", + "r": 1 + } + ], + "texts": [ + { + "x": 48, + "y": 55, + "value": "仓库领域关系 · 定义、内容、任务与节点身份", + "size": 34, + "color": "#172B4D", + "weight": 700, + "maxWidth": 1672 + }, + { + "x": 48, + "y": 91, + "value": "按领域聚合展示主要关联;并非全部物理外键。精确字段、约束与默认值见 v5—v18 Migration。", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 1672 + }, + { + "x": 84, + "y": 190, + "value": "仓库节点", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 440 + }, + { + "x": 84, + "y": 222, + "value": "repository_storage_nodes", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 84, + "y": 249, + "value": "控制 / 发布地址、心跳与文件系统", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 84, + "y": 276, + "value": "Storage Node 身份独立于 TMS Agent", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 660, + "y": 190, + "value": "仓库定义", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 440 + }, + { + "x": 660, + "y": 222, + "value": "software_repositories", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 660, + "y": 249, + "value": "项目 / 产品版本 / 架构 / purpose", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 660, + "y": 276, + "value": "active_revision_id / primary_node", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 190, + "value": "上游来源模板", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 222, + "value": "repository_upstream_profiles", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 249, + "value": "版本 / 架构 / URL 模板 / 默认规则", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 276, + "value": "解析配置,不静默改动已有仓库", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 550, + "y": 215, + "value": "1 → 0..N", + "size": 17, + "color": "#15765C", + "weight": 500, + "maxWidth": 1170 + }, + { + "x": 1128, + "y": 215, + "value": "配置快照", + "size": 17, + "color": "#245EC7", + "weight": 500, + "maxWidth": 592 + }, + { + "x": 84, + "y": 448, + "value": "统一业务任务与执行结果", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 440 + }, + { + "x": 84, + "y": 480, + "value": "repository_tasks", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 84, + "y": 507, + "value": "Task → Targets:0..N", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 84, + "y": 534, + "value": "Task → Result Items:0..N", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 84, + "y": 561, + "value": "Task → Log Index:0..1", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 84, + "y": 588, + "value": "Task → OCAI Analyses:0..N", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 84, + "y": 615, + "value": "lease / fencing / 同仓库 Mutation Lock", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 660, + "y": 448, + "value": "内容会话与不可变版本", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 440 + }, + { + "x": 660, + "y": 480, + "value": "Repository → Revisions:0..N", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 660, + "y": 507, + "value": "Revision → Packages:0..N", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 660, + "y": 534, + "value": "Repository → Upload Sessions", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 660, + "y": 561, + "value": "Upload → Indexed Chunks:0..N", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 660, + "y": 588, + "value": "Quarantine:拒绝包与原因", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 660, + "y": 615, + "value": "RPM 实体 / repodata 留在仓库节点", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 448, + "value": "Node RPM 与注册身份", + "size": 23, + "color": "#A55F16", + "weight": 700, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 480, + "value": "Install Source → 普通 NODE_RPM 仓", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 507, + "value": "Artifact → Package + Revision + SHA", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 534, + "value": "Allowlist(project, Agent) → Enrollment", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 561, + "value": "Enrollment → Token / 授权与 CSR 状态", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 588, + "value": "Certificate 表:唯一公开证书权威", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 615, + "value": "私钥只保存在节点本地", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 366, + "y": 348, + "value": "repository_id", + "size": 17, + "color": "#245EC7", + "weight": 500, + "maxWidth": 1354 + }, + { + "x": 900, + "y": 342, + "value": "active / history", + "size": 17, + "color": "#15765C", + "weight": 500, + "maxWidth": 820 + }, + { + "x": 1186, + "y": 348, + "value": "受保护的标准安装源", + "size": 17, + "color": "#A55F16", + "weight": 500, + "maxWidth": 534 + }, + { + "x": 84, + "y": 842, + "value": "调度、预演与业务作业", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 440 + }, + { + "x": 84, + "y": 874, + "value": "Repository → Schedule:0..1", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 84, + "y": 901, + "value": "Schedule → Schedule Runs → Task", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 84, + "y": 928, + "value": "Preview:Digest / 版本 / 有效期", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 84, + "y": 955, + "value": "Package Install → 同一 Task / Targets", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 84, + "y": 982, + "value": "Inventory Import Job → 同一 Task", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 84, + "y": 1009, + "value": "Chunk 表是接收分片的事实来源", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 660, + "y": 842, + "value": "包校验与元数据签名", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 440 + }, + { + "x": 660, + "y": 874, + "value": "Repository → GPG 公钥元数据", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 660, + "y": 901, + "value": "Node → Metadata Key Inventory", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 660, + "y": 928, + "value": "Repository 仅引用 key_ref", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 660, + "y": 955, + "value": "Revision 固化签名结果与公钥 URL", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 660, + "y": 982, + "value": "旧 Revision 不因换 Key 被重签", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 660, + "y": 1009, + "value": "不保存 / 导出 GPG 私钥", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 842, + "value": "节点占用与安装观测", + "size": 23, + "color": "#A55F16", + "weight": 700, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 874, + "value": "Node → Repository Usages:0..N", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 901, + "value": "logical:active RPM 内容大小", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 928, + "value": "physical:可归属的磁盘分配空间", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 955, + "value": "Node 总量 / 可用量来自 statfs", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 982, + "value": "Agent 能力、架构和 Node RPM 版本", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 1236, + "y": 1009, + "value": "Target 分别保存退出码与失败原因", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 440 + }, + { + "x": 324, + "y": 756, + "value": "Run / Job 关联 Task", + "size": 17, + "color": "#245EC7", + "weight": 500, + "maxWidth": 1396 + }, + { + "x": 900, + "y": 756, + "value": "签名随 Revision 固化", + "size": 17, + "color": "#15765C", + "weight": 500, + "maxWidth": 820 + }, + { + "x": 1470, + "y": 756, + "value": "上线后心跳与目标结果", + "size": 17, + "color": "#A55F16", + "weight": 500, + "maxWidth": 250 + }, + { + "x": 64, + "y": 1203, + "value": "三条权威边界", + "size": 23, + "color": "#172B4D", + "weight": 700, + "maxWidth": 1632 + }, + { + "x": 64, + "y": 1250, + "value": "① 数据库保存定义与索引;RPM、repodata、隔离文件、原始压缩日志保存于 Node。", + "size": 22, + "color": "#172B4D", + "weight": 500, + "maxWidth": 1600 + }, + { + "x": 64, + "y": 1295, + "value": "② Task 管整体状态,Target 管节点结果,Result Item 管逐包结果;证书不设第二套权威模型。", + "size": 22, + "color": "#172B4D", + "weight": 500, + "maxWidth": 1600 + }, + { + "x": 64, + "y": 1340, + "value": "③ Inventory 导出的是定义,不是内容备份;不包含私钥、Bootstrap Token、TMS / OCAI 凭据。", + "size": 22, + "color": "#172B4D", + "weight": 500, + "maxWidth": 1600 + }, + { + "x": 48, + "y": 1424, + "value": "图中“报告”属于结果与导出,不表示另建 validation_reports 表;完整表名、索引和兼容升级规则见数据库设计正文。", + "size": 17, + "color": "#53657B", + "weight": 400, + "maxWidth": 1664 + } + ], + "edges": [ + { + "id": "node-repos", + "from": "nodes", + "to": "repository", + "points": [ + [ + 544, + 232 + ], + [ + 640, + 232 + ] + ], + "color": "#15765C", + "dashed": false, + "reverse": false + }, + { + "id": "template-repo", + "from": "profiles", + "to": "repository", + "points": [ + [ + 1216, + 232 + ], + [ + 1120, + 232 + ] + ], + "color": "#245EC7", + "dashed": true, + "reverse": false + }, + { + "id": "repo-task", + "from": "repository", + "to": "tasks", + "points": [ + [ + 760, + 310 + ], + [ + 760, + 360 + ], + [ + 304, + 360 + ], + [ + 304, + 414 + ] + ], + "color": "#245EC7", + "dashed": false, + "reverse": false + }, + { + "id": "repo-revision", + "from": "repository", + "to": "contents", + "points": [ + [ + 880, + 310 + ], + [ + 880, + 414 + ] + ], + "color": "#15765C", + "dashed": false, + "reverse": false + }, + { + "id": "repo-artifact", + "from": "repository", + "to": "enrollment", + "points": [ + [ + 1000, + 310 + ], + [ + 1000, + 360 + ], + [ + 1456, + 360 + ], + [ + 1456, + 414 + ] + ], + "color": "#A55F16", + "dashed": false, + "reverse": false + }, + { + "id": "run-task", + "from": "workflow", + "to": "tasks", + "points": [ + [ + 304, + 808 + ], + [ + 304, + 692 + ] + ], + "color": "#245EC7", + "dashed": false, + "reverse": false + }, + { + "id": "revision-signature", + "from": "contents", + "to": "signing", + "points": [ + [ + 880, + 692 + ], + [ + 880, + 808 + ] + ], + "color": "#15765C", + "dashed": false, + "reverse": false + }, + { + "id": "enroll-node", + "from": "enrollment", + "to": "usage", + "points": [ + [ + 1456, + 692 + ], + [ + 1456, + 808 + ] + ], + "color": "#A55F16", + "dashed": false, + "reverse": false + } + ] +} diff --git a/docs/images/repository/repo-er-model.svg b/docs/images/repository/repo-er-model.svg new file mode 100644 index 0000000000000000000000000000000000000000..f74819fac8bfd1f9b25006cd44232ccda4431220 --- /dev/null +++ b/docs/images/repository/repo-er-model.svg @@ -0,0 +1,94 @@ + + +仓库领域关系 · 定义、内容、任务与节点身份按领域聚合展示主要关联;并非全部物理外键。精确字段、约束与默认值见 v5—v18 Migration。 + + + + + + + + + + + + + + + + + + + + + +仓库领域关系 · 定义、内容、任务与节点身份 +按领域聚合展示主要关联;并非全部物理外键。精确字段、约束与默认值见 v5—v18 Migration。 +仓库节点 +repository_storage_nodes +控制 / 发布地址、心跳与文件系统 +Storage Node 身份独立于 TMS Agent +仓库定义 +software_repositories +项目 / 产品版本 / 架构 / purpose +active_revision_id / primary_node +上游来源模板 +repository_upstream_profiles +版本 / 架构 / URL 模板 / 默认规则 +解析配置,不静默改动已有仓库 +1 → 0..N +配置快照 +统一业务任务与执行结果 +repository_tasks +Task → Targets:0..N +Task → Result Items:0..N +Task → Log Index:0..1 +Task → OCAI Analyses:0..N +lease / fencing / 同仓库 Mutation Lock +内容会话与不可变版本 +Repository → Revisions:0..N +Revision → Packages:0..N +Repository → Upload Sessions +Upload → Indexed Chunks:0..N +Quarantine:拒绝包与原因 +RPM 实体 / repodata 留在仓库节点 +Node RPM 与注册身份 +Install Source → 普通 NODE_RPM 仓 +Artifact → Package + Revision + SHA +Allowlist(project, Agent) → Enrollment +Enrollment → Token / 授权与 CSR 状态 +Certificate 表:唯一公开证书权威 +私钥只保存在节点本地 +repository_id +active / history +受保护的标准安装源 +调度、预演与业务作业 +Repository → Schedule:0..1 +Schedule → Schedule Runs → Task +Preview:Digest / 版本 / 有效期 +Package Install → 同一 Task / Targets +Inventory Import Job → 同一 Task +Chunk 表是接收分片的事实来源 +包校验与元数据签名 +Repository → GPG 公钥元数据 +Node → Metadata Key Inventory +Repository 仅引用 key_ref +Revision 固化签名结果与公钥 URL +旧 Revision 不因换 Key 被重签 +不保存 / 导出 GPG 私钥 +节点占用与安装观测 +Node → Repository Usages:0..N +logical:active RPM 内容大小 +physical:可归属的磁盘分配空间 +Node 总量 / 可用量来自 statfs +Agent 能力、架构和 Node RPM 版本 +Target 分别保存退出码与失败原因 +Run / Job 关联 Task +签名随 Revision 固化 +上线后心跳与目标结果 +三条权威边界 +① 数据库保存定义与索引;RPM、repodata、隔离文件、原始压缩日志保存于 Node。 +② Task 管整体状态,Target 管节点结果,Result Item 管逐包结果;证书不设第二套权威模型。 +③ Inventory 导出的是定义,不是内容备份;不包含私钥、Bootstrap Token、TMS / OCAI 凭据。 +图中“报告”属于结果与导出,不表示另建 validation_reports 表;完整表名、索引和兼容升级规则见数据库设计正文。 + diff --git a/docs/images/repository/repo-feature-map.arch.json b/docs/images/repository/repo-feature-map.arch.json new file mode 100644 index 0000000000000000000000000000000000000000..3f7a3aa9a72b91fb00f00800f35412330ac99158 --- /dev/null +++ b/docs/images/repository/repo-feature-map.arch.json @@ -0,0 +1,1657 @@ +{ + "name": "repo-feature-map", + "title": "从页面操作到后端执行 · 六条功能路径", + "subtitle": "页面入口、管理协议、共享执行模块与可核验结果逐行对应,覆盖内容管理、节点运维和诊断。", + "width": 1760, + "height": 1400, + "shapes": [ + { + "id": "column-0", + "x": 40, + "y": 124, + "w": 308, + "h": 48, + "fill": "#F1F6FF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "column-1", + "x": 380, + "y": 124, + "w": 434, + "h": 48, + "fill": "#F1F6FF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "column-2", + "x": 846, + "y": 124, + "w": 416, + "h": 48, + "fill": "#EFF8F3", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "column-3", + "x": 1294, + "y": 124, + "w": 426, + "h": 48, + "fill": "#FFF7E9", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r0-c0", + "x": 40, + "y": 200, + "w": 308, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r0-bar0", + "x": 40, + "y": 200, + "w": 4, + "h": 158, + "fill": "#245EC7", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r0-c1", + "x": 380, + "y": 200, + "w": 434, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r0-bar1", + "x": 380, + "y": 200, + "w": 4, + "h": 158, + "fill": "#245EC7", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r0-c2", + "x": 846, + "y": 200, + "w": 416, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r0-bar2", + "x": 846, + "y": 200, + "w": 4, + "h": 158, + "fill": "#15765C", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r0-c3", + "x": 1294, + "y": 200, + "w": 426, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r0-bar3", + "x": 1294, + "y": 200, + "w": 4, + "h": 158, + "fill": "#A55F16", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r1-c0", + "x": 40, + "y": 388, + "w": 308, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r1-bar0", + "x": 40, + "y": 388, + "w": 4, + "h": 158, + "fill": "#245EC7", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r1-c1", + "x": 380, + "y": 388, + "w": 434, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r1-bar1", + "x": 380, + "y": 388, + "w": 4, + "h": 158, + "fill": "#245EC7", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r1-c2", + "x": 846, + "y": 388, + "w": 416, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r1-bar2", + "x": 846, + "y": 388, + "w": 4, + "h": 158, + "fill": "#15765C", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r1-c3", + "x": 1294, + "y": 388, + "w": 426, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r1-bar3", + "x": 1294, + "y": 388, + "w": 4, + "h": 158, + "fill": "#A55F16", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r2-c0", + "x": 40, + "y": 576, + "w": 308, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r2-bar0", + "x": 40, + "y": 576, + "w": 4, + "h": 158, + "fill": "#245EC7", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r2-c1", + "x": 380, + "y": 576, + "w": 434, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r2-bar1", + "x": 380, + "y": 576, + "w": 4, + "h": 158, + "fill": "#245EC7", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r2-c2", + "x": 846, + "y": 576, + "w": 416, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r2-bar2", + "x": 846, + "y": 576, + "w": 4, + "h": 158, + "fill": "#15765C", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r2-c3", + "x": 1294, + "y": 576, + "w": 426, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r2-bar3", + "x": 1294, + "y": 576, + "w": 4, + "h": 158, + "fill": "#A55F16", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r3-c0", + "x": 40, + "y": 764, + "w": 308, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r3-bar0", + "x": 40, + "y": 764, + "w": 4, + "h": 158, + "fill": "#245EC7", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r3-c1", + "x": 380, + "y": 764, + "w": 434, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r3-bar1", + "x": 380, + "y": 764, + "w": 4, + "h": 158, + "fill": "#245EC7", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r3-c2", + "x": 846, + "y": 764, + "w": 416, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r3-bar2", + "x": 846, + "y": 764, + "w": 4, + "h": 158, + "fill": "#15765C", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r3-c3", + "x": 1294, + "y": 764, + "w": 426, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r3-bar3", + "x": 1294, + "y": 764, + "w": 4, + "h": 158, + "fill": "#A55F16", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r4-c0", + "x": 40, + "y": 952, + "w": 308, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r4-bar0", + "x": 40, + "y": 952, + "w": 4, + "h": 158, + "fill": "#245EC7", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r4-c1", + "x": 380, + "y": 952, + "w": 434, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r4-bar1", + "x": 380, + "y": 952, + "w": 4, + "h": 158, + "fill": "#245EC7", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r4-c2", + "x": 846, + "y": 952, + "w": 416, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r4-bar2", + "x": 846, + "y": 952, + "w": 4, + "h": 158, + "fill": "#15765C", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r4-c3", + "x": 1294, + "y": 952, + "w": 426, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r4-bar3", + "x": 1294, + "y": 952, + "w": 4, + "h": 158, + "fill": "#A55F16", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r5-c0", + "x": 40, + "y": 1140, + "w": 308, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r5-bar0", + "x": 40, + "y": 1140, + "w": 4, + "h": 158, + "fill": "#245EC7", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r5-c1", + "x": 380, + "y": 1140, + "w": 434, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r5-bar1", + "x": 380, + "y": 1140, + "w": 4, + "h": 158, + "fill": "#245EC7", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r5-c2", + "x": 846, + "y": 1140, + "w": 416, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r5-bar2", + "x": 846, + "y": 1140, + "w": 4, + "h": 158, + "fill": "#15765C", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r5-c3", + "x": 1294, + "y": 1140, + "w": 426, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r5-bar3", + "x": 1294, + "y": 1140, + "w": 4, + "h": 158, + "fill": "#A55F16", + "stroke": "#D7E1ED", + "r": 1 + } + ], + "texts": [ + { + "x": 48, + "y": 55, + "value": "从页面操作到后端执行 · 六条功能路径", + "size": 34, + "color": "#172B4D", + "weight": 700, + "maxWidth": 1672 + }, + { + "x": 48, + "y": 91, + "value": "页面入口、管理协议、共享执行模块与可核验结果逐行对应,覆盖内容管理、节点运维和诊断。", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 1672 + }, + { + "x": 58, + "y": 155, + "value": "前端页面 / 操作", + "size": 22, + "color": "#245EC7", + "weight": 700, + "maxWidth": 272 + }, + { + "x": 398, + "y": 155, + "value": "真实 Repository RPC", + "size": 22, + "color": "#245EC7", + "weight": 700, + "maxWidth": 398 + }, + { + "x": 864, + "y": 155, + "value": "后端与执行模块", + "size": 22, + "color": "#15765C", + "weight": 700, + "maxWidth": 380 + }, + { + "x": 1312, + "y": 155, + "value": "可见结果 / 验证点", + "size": 22, + "color": "#A55F16", + "weight": 700, + "maxWidth": 390 + }, + { + "x": 60, + "y": 238, + "value": "01", + "size": 18, + "color": "#245EC7", + "weight": 700, + "maxWidth": 1660 + }, + { + "x": 60, + "y": 276, + "value": "仓库管理 / 上游源", + "size": 21, + "color": "#172B4D", + "weight": 600, + "maxWidth": 268 + }, + { + "x": 60, + "y": 310, + "value": "创建、筛选同步、计划同步", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 268 + }, + { + "x": 398, + "y": 240, + "value": "CreateRepository", + "size": 16, + "color": "#172B4D", + "weight": 600, + "maxWidth": 398 + }, + { + "x": 398, + "y": 270, + "value": "UpdateRepository", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 398, + "y": 300, + "value": "SyncRepository", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 398, + "y": 330, + "value": "UpsertRepositorySyncSchedule", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 864, + "y": 240, + "value": "仓库 / 内容 / 调度服务", + "size": 19, + "color": "#172B4D", + "weight": 600, + "maxWidth": 380 + }, + { + "x": 864, + "y": 277, + "value": "原有任务 + Mutation Lock", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 380 + }, + { + "x": 864, + "y": 314, + "value": "Node Worker → 新 Revision", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 380 + }, + { + "x": 1312, + "y": 240, + "value": "源地址与过滤策略可查", + "size": 19, + "color": "#172B4D", + "weight": 600, + "maxWidth": 390 + }, + { + "x": 1312, + "y": 277, + "value": "新包、repodata 与发布状态", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 390 + }, + { + "x": 1312, + "y": 314, + "value": "MIRROR / APPEND 与实际限速", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 390 + }, + { + "x": 60, + "y": 426, + "value": "02", + "size": 18, + "color": "#245EC7", + "weight": 700, + "maxWidth": 1660 + }, + { + "x": 60, + "y": 464, + "value": "软件包 / 上传 Drawer", + "size": 21, + "color": "#172B4D", + "weight": 600, + "maxWidth": 268 + }, + { + "x": 60, + "y": 498, + "value": "多文件、暂停恢复、隔离区", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 268 + }, + { + "x": 398, + "y": 428, + "value": "BeginRepositoryUpload", + "size": 16, + "color": "#172B4D", + "weight": 600, + "maxWidth": 398 + }, + { + "x": 398, + "y": 458, + "value": "PutRepositoryUploadChunk", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 398, + "y": 488, + "value": "CompleteRepositoryUpload", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 398, + "y": 518, + "value": "ListRepositoryQuarantine", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 864, + "y": 428, + "value": "内容服务 + 分片记录", + "size": 19, + "color": "#172B4D", + "weight": 600, + "maxWidth": 380 + }, + { + "x": 864, + "y": 465, + "value": "Node 接收、校验与合并", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 380 + }, + { + "x": 864, + "y": 502, + "value": "RPM / GPG / 归档安全检查", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 380 + }, + { + "x": 1312, + "y": 428, + "value": "缺失分片补传、完整 SHA-256", + "size": 19, + "color": "#172B4D", + "weight": 600, + "maxWidth": 390 + }, + { + "x": 1312, + "y": 465, + "value": "合法包发布;拒绝包隔离", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 390 + }, + { + "x": 1312, + "y": 502, + "value": "tar 部分接收为部分成功", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 390 + }, + { + "x": 60, + "y": 614, + "value": "03", + "size": 18, + "color": "#245EC7", + "weight": 700, + "maxWidth": 1660 + }, + { + "x": 60, + "y": 652, + "value": "软件包 / 安装与删除", + "size": 21, + "color": "#172B4D", + "weight": 600, + "maxWidth": 268 + }, + { + "x": 60, + "y": 686, + "value": "先预演,再确认执行", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 268 + }, + { + "x": 398, + "y": 616, + "value": "PreviewRepositoryPackageInstall", + "size": 16, + "color": "#172B4D", + "weight": 600, + "maxWidth": 398 + }, + { + "x": 398, + "y": 646, + "value": "ConfirmRepositoryPackageInstall", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 398, + "y": 676, + "value": "PreviewRepositoryPackageDeletion", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 398, + "y": 706, + "value": "ConfirmRepositoryPackageDeletion", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 864, + "y": 616, + "value": "预演摘要 + 不可变版本", + "size": 19, + "color": "#172B4D", + "weight": 600, + "maxWidth": 380 + }, + { + "x": 864, + "y": 646, + "value": "Task Target / Result Item", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 380 + }, + { + "x": 864, + "y": 676, + "value": "安装:现有 TMS + DNF", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 380 + }, + { + "x": 864, + "y": 706, + "value": "删除:Worker + 单次发布", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 380 + }, + { + "x": 1312, + "y": 616, + "value": "各目标独立安装结果", + "size": 19, + "color": "#172B4D", + "weight": 600, + "maxWidth": 390 + }, + { + "x": 1312, + "y": 653, + "value": "直接 / 传递依赖影响可见", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 390 + }, + { + "x": 1312, + "y": 690, + "value": "批量删包只重建一次元数据", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 390 + }, + { + "x": 60, + "y": 802, + "value": "04", + "size": 18, + "color": "#245EC7", + "weight": 700, + "maxWidth": 1660 + }, + { + "x": 60, + "y": 840, + "value": "仓库节点 / 节点运维", + "size": 21, + "color": "#172B4D", + "weight": 600, + "maxWidth": 268 + }, + { + "x": 60, + "y": 874, + "value": "容量、同步、安装与白名单", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 268 + }, + { + "x": 398, + "y": 804, + "value": "DescribeRepositoryStorageNode", + "size": 16, + "color": "#172B4D", + "weight": 600, + "maxWidth": 398 + }, + { + "x": 398, + "y": 834, + "value": "ConfigureRepositoryNodeInstallSource", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 398, + "y": 864, + "value": "CreateRepositoryNodeEnrollment", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 398, + "y": 894, + "value": "SyncRepository", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 864, + "y": 804, + "value": "节点 / 预检 / 占用服务", + "size": 19, + "color": "#172B4D", + "weight": 600, + "maxWidth": 380 + }, + { + "x": 864, + "y": 841, + "value": "正式 Node RPM 源 + 现有 TMS", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 380 + }, + { + "x": 864, + "y": 878, + "value": "白名单 / Bootstrap / 证书", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 380 + }, + { + "x": 1312, + "y": 804, + "value": "逻辑大小与物理占用分开", + "size": 19, + "color": "#172B4D", + "weight": 600, + "maxWidth": 390 + }, + { + "x": 1312, + "y": 841, + "value": "EVR 最新 / 历史版本可选", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 390 + }, + { + "x": 1312, + "y": 878, + "value": "标准源 DNF 安装后自动注册", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 390 + }, + { + "x": 60, + "y": 990, + "value": "05", + "size": 18, + "color": "#245EC7", + "weight": 700, + "maxWidth": 1660 + }, + { + "x": 60, + "y": 1028, + "value": "任务总览 / Task Drawer", + "size": 21, + "color": "#172B4D", + "weight": 600, + "maxWidth": 268 + }, + { + "x": 60, + "y": 1062, + "value": "筛选、日志、诊断与 AI", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 268 + }, + { + "x": 398, + "y": 992, + "value": "ListRepositoryTasks", + "size": 16, + "color": "#172B4D", + "weight": 600, + "maxWidth": 398 + }, + { + "x": 398, + "y": 1022, + "value": "DescribeRepositoryTask", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 398, + "y": 1052, + "value": "DownloadRepositoryTaskLog", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 398, + "y": 1082, + "value": "CreateRepositoryOCAIAnalysis", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 864, + "y": 992, + "value": "任务服务 / 统一错误目录", + "size": 19, + "color": "#172B4D", + "weight": 600, + "maxWidth": 380 + }, + { + "x": 864, + "y": 1029, + "value": "节点 JSONL.zst + 脱敏 Bundle", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 380 + }, + { + "x": 864, + "y": 1066, + "value": "上下文服务 + 现有 OCAI SSE", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 380 + }, + { + "x": 1312, + "y": 992, + "value": "四态统计与 Target 结果", + "size": 19, + "color": "#172B4D", + "weight": 600, + "maxWidth": 390 + }, + { + "x": 1312, + "y": 1029, + "value": "英文 Code + 中文原因 / 建议", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 390 + }, + { + "x": 1312, + "y": 1066, + "value": "真实只读分析,不自动修复", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 390 + }, + { + "x": 60, + "y": 1178, + "value": "06", + "size": 18, + "color": "#245EC7", + "weight": 700, + "maxWidth": 1660 + }, + { + "x": 60, + "y": 1216, + "value": "仓库配置 / 分发 / 导入", + "size": 21, + "color": "#172B4D", + "weight": 600, + "maxWidth": 268 + }, + { + "x": 60, + "y": 1250, + "value": "生成配置、目标分发、定义导入", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 268 + }, + { + "x": 398, + "y": 1180, + "value": "GenerateRepositoryConfig", + "size": 16, + "color": "#172B4D", + "weight": 600, + "maxWidth": 398 + }, + { + "x": 398, + "y": 1210, + "value": "CreateRepositoryDistribution", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 398, + "y": 1240, + "value": "PreviewRepositoryInventoryImport", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 398, + "y": 1270, + "value": "ConfirmRepositoryInventoryImport", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 864, + "y": 1180, + "value": "配置生成 / 分发 / 导入服务", + "size": 19, + "color": "#172B4D", + "weight": 600, + "maxWidth": 380 + }, + { + "x": 864, + "y": 1217, + "value": "Revision + manifest 绑定", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 380 + }, + { + "x": 864, + "y": 1254, + "value": "原有 TMS / 统一 Repository Task", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 380 + }, + { + "x": 1312, + "y": 1180, + "value": "复制 / 下载 .repo 后真实 DNF", + "size": 19, + "color": "#172B4D", + "weight": 600, + "maxWidth": 390 + }, + { + "x": 1312, + "y": 1217, + "value": "目标校验快照并原子切换", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 390 + }, + { + "x": 1312, + "y": 1254, + "value": "定义可复用;不导出 Secret", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 390 + }, + { + "x": 48, + "y": 1374, + "value": "页面:manager/frontend/src/pages/repository;协议与实现:manager/backend/{protocol/repository, services/footstone}。RPC 细节以 Proto 为准。", + "size": 17, + "color": "#53657B", + "weight": 400, + "maxWidth": 1664 + } + ], + "edges": [ + { + "id": "r0-link0", + "from": "r0-c0", + "to": "r0-c1", + "points": [ + [ + 348, + 279 + ], + [ + 380, + 279 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r0-link1", + "from": "r0-c1", + "to": "r0-c2", + "points": [ + [ + 814, + 279 + ], + [ + 846, + 279 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r0-link2", + "from": "r0-c2", + "to": "r0-c3", + "points": [ + [ + 1262, + 279 + ], + [ + 1294, + 279 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r1-link0", + "from": "r1-c0", + "to": "r1-c1", + "points": [ + [ + 348, + 467 + ], + [ + 380, + 467 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r1-link1", + "from": "r1-c1", + "to": "r1-c2", + "points": [ + [ + 814, + 467 + ], + [ + 846, + 467 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r1-link2", + "from": "r1-c2", + "to": "r1-c3", + "points": [ + [ + 1262, + 467 + ], + [ + 1294, + 467 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r2-link0", + "from": "r2-c0", + "to": "r2-c1", + "points": [ + [ + 348, + 655 + ], + [ + 380, + 655 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r2-link1", + "from": "r2-c1", + "to": "r2-c2", + "points": [ + [ + 814, + 655 + ], + [ + 846, + 655 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r2-link2", + "from": "r2-c2", + "to": "r2-c3", + "points": [ + [ + 1262, + 655 + ], + [ + 1294, + 655 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r3-link0", + "from": "r3-c0", + "to": "r3-c1", + "points": [ + [ + 348, + 843 + ], + [ + 380, + 843 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r3-link1", + "from": "r3-c1", + "to": "r3-c2", + "points": [ + [ + 814, + 843 + ], + [ + 846, + 843 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r3-link2", + "from": "r3-c2", + "to": "r3-c3", + "points": [ + [ + 1262, + 843 + ], + [ + 1294, + 843 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r4-link0", + "from": "r4-c0", + "to": "r4-c1", + "points": [ + [ + 348, + 1031 + ], + [ + 380, + 1031 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r4-link1", + "from": "r4-c1", + "to": "r4-c2", + "points": [ + [ + 814, + 1031 + ], + [ + 846, + 1031 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r4-link2", + "from": "r4-c2", + "to": "r4-c3", + "points": [ + [ + 1262, + 1031 + ], + [ + 1294, + 1031 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r5-link0", + "from": "r5-c0", + "to": "r5-c1", + "points": [ + [ + 348, + 1219 + ], + [ + 380, + 1219 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r5-link1", + "from": "r5-c1", + "to": "r5-c2", + "points": [ + [ + 814, + 1219 + ], + [ + 846, + 1219 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r5-link2", + "from": "r5-c2", + "to": "r5-c3", + "points": [ + [ + 1262, + 1219 + ], + [ + 1294, + 1219 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + } + ], + "apiReferences": [ + "CreateRepository", + "UpdateRepository", + "SyncRepository", + "UpsertRepositorySyncSchedule", + "BeginRepositoryUpload", + "PutRepositoryUploadChunk", + "CompleteRepositoryUpload", + "ListRepositoryQuarantine", + "PreviewRepositoryPackageInstall", + "ConfirmRepositoryPackageInstall", + "PreviewRepositoryPackageDeletion", + "ConfirmRepositoryPackageDeletion", + "DescribeRepositoryStorageNode", + "ConfigureRepositoryNodeInstallSource", + "CreateRepositoryNodeEnrollment", + "SyncRepository", + "ListRepositoryTasks", + "DescribeRepositoryTask", + "DownloadRepositoryTaskLog", + "CreateRepositoryOCAIAnalysis", + "GenerateRepositoryConfig", + "CreateRepositoryDistribution", + "PreviewRepositoryInventoryImport", + "ConfirmRepositoryInventoryImport" + ] +} diff --git a/docs/images/repository/repo-feature-map.dot b/docs/images/repository/repo-feature-map.dot new file mode 100644 index 0000000000000000000000000000000000000000..72b16005e62dbf017a66916c54d2af25aab474f1 --- /dev/null +++ b/docs/images/repository/repo-feature-map.dot @@ -0,0 +1,52 @@ +// Logical edge reference generated by scripts/render-repository-diagrams.mjs. +// The SVG / drawio files retain the reviewed fixed layout. +digraph repo_feature_map { + graph [rankdir=LR, fontname="Microsoft YaHei"]; + node [shape=box, fontname="Microsoft YaHei"]; + "column-0" [label="前端页面 / 操作"]; + "column-1" [label="真实 Repository RPC"]; + "column-2" [label="后端与执行模块"]; + "column-3" [label="可见结果 / 验证点"]; + "r0-c0" [label="01\n仓库管理 / 上游源\n创建、筛选同步、计划同步"]; + "r0-c1" [label="CreateRepository\nUpdateRepository\nSyncRepository\nUpsertRepositorySyncSchedule"]; + "r0-c2" [label="仓库 / 内容 / 调度服务\n原有任务 + Mutation Lock\nNode Worker → 新 Revision"]; + "r0-c3" [label="源地址与过滤策略可查\n新包、repodata 与发布状态\nMIRROR / APPEND 与实际限速"]; + "r1-c0" [label="02\n软件包 / 上传 Drawer\n多文件、暂停恢复、隔离区"]; + "r1-c1" [label="BeginRepositoryUpload\nPutRepositoryUploadChunk\nCompleteRepositoryUpload\nListRepositoryQuarantine"]; + "r1-c2" [label="内容服务 + 分片记录\nNode 接收、校验与合并\nRPM / GPG / 归档安全检查"]; + "r1-c3" [label="缺失分片补传、完整 SHA-256\n合法包发布;拒绝包隔离\ntar 部分接收为部分成功"]; + "r2-c0" [label="03\n软件包 / 安装与删除\n先预演,再确认执行"]; + "r2-c1" [label="PreviewRepositoryPackageInstall\nConfirmRepositoryPackageInstall\nPreviewRepositoryPackageDeletion\nConfirmRepositoryPackageDeletion"]; + "r2-c2" [label="预演摘要 + 不可变版本\nTask Target / Result Item\n安装:现有 TMS + DNF\n删除:Worker + 单次发布"]; + "r2-c3" [label="各目标独立安装结果\n直接 / 传递依赖影响可见\n批量删包只重建一次元数据"]; + "r3-c0" [label="04\n仓库节点 / 节点运维\n容量、同步、安装与白名单"]; + "r3-c1" [label="DescribeRepositoryStorageNode\nConfigureRepositoryNodeInstallSource\nCreateRepositoryNodeEnrollment\nSyncRepository"]; + "r3-c2" [label="节点 / 预检 / 占用服务\n正式 Node RPM 源 + 现有 TMS\n白名单 / Bootstrap / 证书"]; + "r3-c3" [label="逻辑大小与物理占用分开\nEVR 最新 / 历史版本可选\n标准源 DNF 安装后自动注册"]; + "r4-c0" [label="05\n任务总览 / Task Drawer\n筛选、日志、诊断与 AI"]; + "r4-c1" [label="ListRepositoryTasks\nDescribeRepositoryTask\nDownloadRepositoryTaskLog\nCreateRepositoryOCAIAnalysis"]; + "r4-c2" [label="任务服务 / 统一错误目录\n节点 JSONL.zst + 脱敏 Bundle\n上下文服务 + 现有 OCAI SSE"]; + "r4-c3" [label="四态统计与 Target 结果\n英文 Code + 中文原因 / 建议\n真实只读分析,不自动修复"]; + "r5-c0" [label="06\n仓库配置 / 分发 / 导入\n生成配置、目标分发、定义导入"]; + "r5-c1" [label="GenerateRepositoryConfig\nCreateRepositoryDistribution\nPreviewRepositoryInventoryImport\nConfirmRepositoryInventoryImport"]; + "r5-c2" [label="配置生成 / 分发 / 导入服务\nRevision + manifest 绑定\n原有 TMS / 统一 Repository Task"]; + "r5-c3" [label="复制 / 下载 .repo 后真实 DNF\n目标校验快照并原子切换\n定义可复用;不导出 Secret"]; + "r0-c0" -> "r0-c1" [color="#8B9BB0"]; + "r0-c1" -> "r0-c2" [color="#8B9BB0"]; + "r0-c2" -> "r0-c3" [color="#8B9BB0"]; + "r1-c0" -> "r1-c1" [color="#8B9BB0"]; + "r1-c1" -> "r1-c2" [color="#8B9BB0"]; + "r1-c2" -> "r1-c3" [color="#8B9BB0"]; + "r2-c0" -> "r2-c1" [color="#8B9BB0"]; + "r2-c1" -> "r2-c2" [color="#8B9BB0"]; + "r2-c2" -> "r2-c3" [color="#8B9BB0"]; + "r3-c0" -> "r3-c1" [color="#8B9BB0"]; + "r3-c1" -> "r3-c2" [color="#8B9BB0"]; + "r3-c2" -> "r3-c3" [color="#8B9BB0"]; + "r4-c0" -> "r4-c1" [color="#8B9BB0"]; + "r4-c1" -> "r4-c2" [color="#8B9BB0"]; + "r4-c2" -> "r4-c3" [color="#8B9BB0"]; + "r5-c0" -> "r5-c1" [color="#8B9BB0"]; + "r5-c1" -> "r5-c2" [color="#8B9BB0"]; + "r5-c2" -> "r5-c3" [color="#8B9BB0"]; +} diff --git a/docs/images/repository/repo-feature-map.drawio b/docs/images/repository/repo-feature-map.drawio new file mode 100644 index 0000000000000000000000000000000000000000..a63ffffa30cb377d387fe196a8115f81fb0cac79 --- /dev/null +++ b/docs/images/repository/repo-feature-map.drawio @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/repository/repo-feature-map.png b/docs/images/repository/repo-feature-map.png new file mode 100644 index 0000000000000000000000000000000000000000..c3d1f1c1f65c3e2d54287a72435b7e9a12a1fc99 Binary files /dev/null and b/docs/images/repository/repo-feature-map.png differ diff --git a/docs/images/repository/repo-feature-map.spec.yaml b/docs/images/repository/repo-feature-map.spec.yaml new file mode 100644 index 0000000000000000000000000000000000000000..3f7a3aa9a72b91fb00f00800f35412330ac99158 --- /dev/null +++ b/docs/images/repository/repo-feature-map.spec.yaml @@ -0,0 +1,1657 @@ +{ + "name": "repo-feature-map", + "title": "从页面操作到后端执行 · 六条功能路径", + "subtitle": "页面入口、管理协议、共享执行模块与可核验结果逐行对应,覆盖内容管理、节点运维和诊断。", + "width": 1760, + "height": 1400, + "shapes": [ + { + "id": "column-0", + "x": 40, + "y": 124, + "w": 308, + "h": 48, + "fill": "#F1F6FF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "column-1", + "x": 380, + "y": 124, + "w": 434, + "h": 48, + "fill": "#F1F6FF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "column-2", + "x": 846, + "y": 124, + "w": 416, + "h": 48, + "fill": "#EFF8F3", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "column-3", + "x": 1294, + "y": 124, + "w": 426, + "h": 48, + "fill": "#FFF7E9", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r0-c0", + "x": 40, + "y": 200, + "w": 308, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r0-bar0", + "x": 40, + "y": 200, + "w": 4, + "h": 158, + "fill": "#245EC7", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r0-c1", + "x": 380, + "y": 200, + "w": 434, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r0-bar1", + "x": 380, + "y": 200, + "w": 4, + "h": 158, + "fill": "#245EC7", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r0-c2", + "x": 846, + "y": 200, + "w": 416, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r0-bar2", + "x": 846, + "y": 200, + "w": 4, + "h": 158, + "fill": "#15765C", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r0-c3", + "x": 1294, + "y": 200, + "w": 426, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r0-bar3", + "x": 1294, + "y": 200, + "w": 4, + "h": 158, + "fill": "#A55F16", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r1-c0", + "x": 40, + "y": 388, + "w": 308, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r1-bar0", + "x": 40, + "y": 388, + "w": 4, + "h": 158, + "fill": "#245EC7", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r1-c1", + "x": 380, + "y": 388, + "w": 434, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r1-bar1", + "x": 380, + "y": 388, + "w": 4, + "h": 158, + "fill": "#245EC7", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r1-c2", + "x": 846, + "y": 388, + "w": 416, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r1-bar2", + "x": 846, + "y": 388, + "w": 4, + "h": 158, + "fill": "#15765C", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r1-c3", + "x": 1294, + "y": 388, + "w": 426, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r1-bar3", + "x": 1294, + "y": 388, + "w": 4, + "h": 158, + "fill": "#A55F16", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r2-c0", + "x": 40, + "y": 576, + "w": 308, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r2-bar0", + "x": 40, + "y": 576, + "w": 4, + "h": 158, + "fill": "#245EC7", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r2-c1", + "x": 380, + "y": 576, + "w": 434, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r2-bar1", + "x": 380, + "y": 576, + "w": 4, + "h": 158, + "fill": "#245EC7", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r2-c2", + "x": 846, + "y": 576, + "w": 416, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r2-bar2", + "x": 846, + "y": 576, + "w": 4, + "h": 158, + "fill": "#15765C", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r2-c3", + "x": 1294, + "y": 576, + "w": 426, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r2-bar3", + "x": 1294, + "y": 576, + "w": 4, + "h": 158, + "fill": "#A55F16", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r3-c0", + "x": 40, + "y": 764, + "w": 308, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r3-bar0", + "x": 40, + "y": 764, + "w": 4, + "h": 158, + "fill": "#245EC7", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r3-c1", + "x": 380, + "y": 764, + "w": 434, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r3-bar1", + "x": 380, + "y": 764, + "w": 4, + "h": 158, + "fill": "#245EC7", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r3-c2", + "x": 846, + "y": 764, + "w": 416, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r3-bar2", + "x": 846, + "y": 764, + "w": 4, + "h": 158, + "fill": "#15765C", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r3-c3", + "x": 1294, + "y": 764, + "w": 426, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r3-bar3", + "x": 1294, + "y": 764, + "w": 4, + "h": 158, + "fill": "#A55F16", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r4-c0", + "x": 40, + "y": 952, + "w": 308, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r4-bar0", + "x": 40, + "y": 952, + "w": 4, + "h": 158, + "fill": "#245EC7", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r4-c1", + "x": 380, + "y": 952, + "w": 434, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r4-bar1", + "x": 380, + "y": 952, + "w": 4, + "h": 158, + "fill": "#245EC7", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r4-c2", + "x": 846, + "y": 952, + "w": 416, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r4-bar2", + "x": 846, + "y": 952, + "w": 4, + "h": 158, + "fill": "#15765C", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r4-c3", + "x": 1294, + "y": 952, + "w": 426, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r4-bar3", + "x": 1294, + "y": 952, + "w": 4, + "h": 158, + "fill": "#A55F16", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r5-c0", + "x": 40, + "y": 1140, + "w": 308, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r5-bar0", + "x": 40, + "y": 1140, + "w": 4, + "h": 158, + "fill": "#245EC7", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r5-c1", + "x": 380, + "y": 1140, + "w": 434, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r5-bar1", + "x": 380, + "y": 1140, + "w": 4, + "h": 158, + "fill": "#245EC7", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r5-c2", + "x": 846, + "y": 1140, + "w": 416, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r5-bar2", + "x": 846, + "y": 1140, + "w": 4, + "h": 158, + "fill": "#15765C", + "stroke": "#D7E1ED", + "r": 1 + }, + { + "id": "r5-c3", + "x": 1294, + "y": 1140, + "w": 426, + "h": 158, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "r5-bar3", + "x": 1294, + "y": 1140, + "w": 4, + "h": 158, + "fill": "#A55F16", + "stroke": "#D7E1ED", + "r": 1 + } + ], + "texts": [ + { + "x": 48, + "y": 55, + "value": "从页面操作到后端执行 · 六条功能路径", + "size": 34, + "color": "#172B4D", + "weight": 700, + "maxWidth": 1672 + }, + { + "x": 48, + "y": 91, + "value": "页面入口、管理协议、共享执行模块与可核验结果逐行对应,覆盖内容管理、节点运维和诊断。", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 1672 + }, + { + "x": 58, + "y": 155, + "value": "前端页面 / 操作", + "size": 22, + "color": "#245EC7", + "weight": 700, + "maxWidth": 272 + }, + { + "x": 398, + "y": 155, + "value": "真实 Repository RPC", + "size": 22, + "color": "#245EC7", + "weight": 700, + "maxWidth": 398 + }, + { + "x": 864, + "y": 155, + "value": "后端与执行模块", + "size": 22, + "color": "#15765C", + "weight": 700, + "maxWidth": 380 + }, + { + "x": 1312, + "y": 155, + "value": "可见结果 / 验证点", + "size": 22, + "color": "#A55F16", + "weight": 700, + "maxWidth": 390 + }, + { + "x": 60, + "y": 238, + "value": "01", + "size": 18, + "color": "#245EC7", + "weight": 700, + "maxWidth": 1660 + }, + { + "x": 60, + "y": 276, + "value": "仓库管理 / 上游源", + "size": 21, + "color": "#172B4D", + "weight": 600, + "maxWidth": 268 + }, + { + "x": 60, + "y": 310, + "value": "创建、筛选同步、计划同步", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 268 + }, + { + "x": 398, + "y": 240, + "value": "CreateRepository", + "size": 16, + "color": "#172B4D", + "weight": 600, + "maxWidth": 398 + }, + { + "x": 398, + "y": 270, + "value": "UpdateRepository", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 398, + "y": 300, + "value": "SyncRepository", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 398, + "y": 330, + "value": "UpsertRepositorySyncSchedule", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 864, + "y": 240, + "value": "仓库 / 内容 / 调度服务", + "size": 19, + "color": "#172B4D", + "weight": 600, + "maxWidth": 380 + }, + { + "x": 864, + "y": 277, + "value": "原有任务 + Mutation Lock", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 380 + }, + { + "x": 864, + "y": 314, + "value": "Node Worker → 新 Revision", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 380 + }, + { + "x": 1312, + "y": 240, + "value": "源地址与过滤策略可查", + "size": 19, + "color": "#172B4D", + "weight": 600, + "maxWidth": 390 + }, + { + "x": 1312, + "y": 277, + "value": "新包、repodata 与发布状态", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 390 + }, + { + "x": 1312, + "y": 314, + "value": "MIRROR / APPEND 与实际限速", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 390 + }, + { + "x": 60, + "y": 426, + "value": "02", + "size": 18, + "color": "#245EC7", + "weight": 700, + "maxWidth": 1660 + }, + { + "x": 60, + "y": 464, + "value": "软件包 / 上传 Drawer", + "size": 21, + "color": "#172B4D", + "weight": 600, + "maxWidth": 268 + }, + { + "x": 60, + "y": 498, + "value": "多文件、暂停恢复、隔离区", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 268 + }, + { + "x": 398, + "y": 428, + "value": "BeginRepositoryUpload", + "size": 16, + "color": "#172B4D", + "weight": 600, + "maxWidth": 398 + }, + { + "x": 398, + "y": 458, + "value": "PutRepositoryUploadChunk", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 398, + "y": 488, + "value": "CompleteRepositoryUpload", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 398, + "y": 518, + "value": "ListRepositoryQuarantine", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 864, + "y": 428, + "value": "内容服务 + 分片记录", + "size": 19, + "color": "#172B4D", + "weight": 600, + "maxWidth": 380 + }, + { + "x": 864, + "y": 465, + "value": "Node 接收、校验与合并", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 380 + }, + { + "x": 864, + "y": 502, + "value": "RPM / GPG / 归档安全检查", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 380 + }, + { + "x": 1312, + "y": 428, + "value": "缺失分片补传、完整 SHA-256", + "size": 19, + "color": "#172B4D", + "weight": 600, + "maxWidth": 390 + }, + { + "x": 1312, + "y": 465, + "value": "合法包发布;拒绝包隔离", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 390 + }, + { + "x": 1312, + "y": 502, + "value": "tar 部分接收为部分成功", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 390 + }, + { + "x": 60, + "y": 614, + "value": "03", + "size": 18, + "color": "#245EC7", + "weight": 700, + "maxWidth": 1660 + }, + { + "x": 60, + "y": 652, + "value": "软件包 / 安装与删除", + "size": 21, + "color": "#172B4D", + "weight": 600, + "maxWidth": 268 + }, + { + "x": 60, + "y": 686, + "value": "先预演,再确认执行", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 268 + }, + { + "x": 398, + "y": 616, + "value": "PreviewRepositoryPackageInstall", + "size": 16, + "color": "#172B4D", + "weight": 600, + "maxWidth": 398 + }, + { + "x": 398, + "y": 646, + "value": "ConfirmRepositoryPackageInstall", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 398, + "y": 676, + "value": "PreviewRepositoryPackageDeletion", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 398, + "y": 706, + "value": "ConfirmRepositoryPackageDeletion", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 864, + "y": 616, + "value": "预演摘要 + 不可变版本", + "size": 19, + "color": "#172B4D", + "weight": 600, + "maxWidth": 380 + }, + { + "x": 864, + "y": 646, + "value": "Task Target / Result Item", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 380 + }, + { + "x": 864, + "y": 676, + "value": "安装:现有 TMS + DNF", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 380 + }, + { + "x": 864, + "y": 706, + "value": "删除:Worker + 单次发布", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 380 + }, + { + "x": 1312, + "y": 616, + "value": "各目标独立安装结果", + "size": 19, + "color": "#172B4D", + "weight": 600, + "maxWidth": 390 + }, + { + "x": 1312, + "y": 653, + "value": "直接 / 传递依赖影响可见", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 390 + }, + { + "x": 1312, + "y": 690, + "value": "批量删包只重建一次元数据", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 390 + }, + { + "x": 60, + "y": 802, + "value": "04", + "size": 18, + "color": "#245EC7", + "weight": 700, + "maxWidth": 1660 + }, + { + "x": 60, + "y": 840, + "value": "仓库节点 / 节点运维", + "size": 21, + "color": "#172B4D", + "weight": 600, + "maxWidth": 268 + }, + { + "x": 60, + "y": 874, + "value": "容量、同步、安装与白名单", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 268 + }, + { + "x": 398, + "y": 804, + "value": "DescribeRepositoryStorageNode", + "size": 16, + "color": "#172B4D", + "weight": 600, + "maxWidth": 398 + }, + { + "x": 398, + "y": 834, + "value": "ConfigureRepositoryNodeInstallSource", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 398, + "y": 864, + "value": "CreateRepositoryNodeEnrollment", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 398, + "y": 894, + "value": "SyncRepository", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 864, + "y": 804, + "value": "节点 / 预检 / 占用服务", + "size": 19, + "color": "#172B4D", + "weight": 600, + "maxWidth": 380 + }, + { + "x": 864, + "y": 841, + "value": "正式 Node RPM 源 + 现有 TMS", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 380 + }, + { + "x": 864, + "y": 878, + "value": "白名单 / Bootstrap / 证书", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 380 + }, + { + "x": 1312, + "y": 804, + "value": "逻辑大小与物理占用分开", + "size": 19, + "color": "#172B4D", + "weight": 600, + "maxWidth": 390 + }, + { + "x": 1312, + "y": 841, + "value": "EVR 最新 / 历史版本可选", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 390 + }, + { + "x": 1312, + "y": 878, + "value": "标准源 DNF 安装后自动注册", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 390 + }, + { + "x": 60, + "y": 990, + "value": "05", + "size": 18, + "color": "#245EC7", + "weight": 700, + "maxWidth": 1660 + }, + { + "x": 60, + "y": 1028, + "value": "任务总览 / Task Drawer", + "size": 21, + "color": "#172B4D", + "weight": 600, + "maxWidth": 268 + }, + { + "x": 60, + "y": 1062, + "value": "筛选、日志、诊断与 AI", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 268 + }, + { + "x": 398, + "y": 992, + "value": "ListRepositoryTasks", + "size": 16, + "color": "#172B4D", + "weight": 600, + "maxWidth": 398 + }, + { + "x": 398, + "y": 1022, + "value": "DescribeRepositoryTask", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 398, + "y": 1052, + "value": "DownloadRepositoryTaskLog", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 398, + "y": 1082, + "value": "CreateRepositoryOCAIAnalysis", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 864, + "y": 992, + "value": "任务服务 / 统一错误目录", + "size": 19, + "color": "#172B4D", + "weight": 600, + "maxWidth": 380 + }, + { + "x": 864, + "y": 1029, + "value": "节点 JSONL.zst + 脱敏 Bundle", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 380 + }, + { + "x": 864, + "y": 1066, + "value": "上下文服务 + 现有 OCAI SSE", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 380 + }, + { + "x": 1312, + "y": 992, + "value": "四态统计与 Target 结果", + "size": 19, + "color": "#172B4D", + "weight": 600, + "maxWidth": 390 + }, + { + "x": 1312, + "y": 1029, + "value": "英文 Code + 中文原因 / 建议", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 390 + }, + { + "x": 1312, + "y": 1066, + "value": "真实只读分析,不自动修复", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 390 + }, + { + "x": 60, + "y": 1178, + "value": "06", + "size": 18, + "color": "#245EC7", + "weight": 700, + "maxWidth": 1660 + }, + { + "x": 60, + "y": 1216, + "value": "仓库配置 / 分发 / 导入", + "size": 21, + "color": "#172B4D", + "weight": 600, + "maxWidth": 268 + }, + { + "x": 60, + "y": 1250, + "value": "生成配置、目标分发、定义导入", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 268 + }, + { + "x": 398, + "y": 1180, + "value": "GenerateRepositoryConfig", + "size": 16, + "color": "#172B4D", + "weight": 600, + "maxWidth": 398 + }, + { + "x": 398, + "y": 1210, + "value": "CreateRepositoryDistribution", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 398, + "y": 1240, + "value": "PreviewRepositoryInventoryImport", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 398, + "y": 1270, + "value": "ConfirmRepositoryInventoryImport", + "size": 16, + "color": "#53657B", + "weight": 400, + "maxWidth": 398 + }, + { + "x": 864, + "y": 1180, + "value": "配置生成 / 分发 / 导入服务", + "size": 19, + "color": "#172B4D", + "weight": 600, + "maxWidth": 380 + }, + { + "x": 864, + "y": 1217, + "value": "Revision + manifest 绑定", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 380 + }, + { + "x": 864, + "y": 1254, + "value": "原有 TMS / 统一 Repository Task", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 380 + }, + { + "x": 1312, + "y": 1180, + "value": "复制 / 下载 .repo 后真实 DNF", + "size": 19, + "color": "#172B4D", + "weight": 600, + "maxWidth": 390 + }, + { + "x": 1312, + "y": 1217, + "value": "目标校验快照并原子切换", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 390 + }, + { + "x": 1312, + "y": 1254, + "value": "定义可复用;不导出 Secret", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 390 + }, + { + "x": 48, + "y": 1374, + "value": "页面:manager/frontend/src/pages/repository;协议与实现:manager/backend/{protocol/repository, services/footstone}。RPC 细节以 Proto 为准。", + "size": 17, + "color": "#53657B", + "weight": 400, + "maxWidth": 1664 + } + ], + "edges": [ + { + "id": "r0-link0", + "from": "r0-c0", + "to": "r0-c1", + "points": [ + [ + 348, + 279 + ], + [ + 380, + 279 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r0-link1", + "from": "r0-c1", + "to": "r0-c2", + "points": [ + [ + 814, + 279 + ], + [ + 846, + 279 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r0-link2", + "from": "r0-c2", + "to": "r0-c3", + "points": [ + [ + 1262, + 279 + ], + [ + 1294, + 279 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r1-link0", + "from": "r1-c0", + "to": "r1-c1", + "points": [ + [ + 348, + 467 + ], + [ + 380, + 467 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r1-link1", + "from": "r1-c1", + "to": "r1-c2", + "points": [ + [ + 814, + 467 + ], + [ + 846, + 467 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r1-link2", + "from": "r1-c2", + "to": "r1-c3", + "points": [ + [ + 1262, + 467 + ], + [ + 1294, + 467 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r2-link0", + "from": "r2-c0", + "to": "r2-c1", + "points": [ + [ + 348, + 655 + ], + [ + 380, + 655 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r2-link1", + "from": "r2-c1", + "to": "r2-c2", + "points": [ + [ + 814, + 655 + ], + [ + 846, + 655 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r2-link2", + "from": "r2-c2", + "to": "r2-c3", + "points": [ + [ + 1262, + 655 + ], + [ + 1294, + 655 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r3-link0", + "from": "r3-c0", + "to": "r3-c1", + "points": [ + [ + 348, + 843 + ], + [ + 380, + 843 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r3-link1", + "from": "r3-c1", + "to": "r3-c2", + "points": [ + [ + 814, + 843 + ], + [ + 846, + 843 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r3-link2", + "from": "r3-c2", + "to": "r3-c3", + "points": [ + [ + 1262, + 843 + ], + [ + 1294, + 843 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r4-link0", + "from": "r4-c0", + "to": "r4-c1", + "points": [ + [ + 348, + 1031 + ], + [ + 380, + 1031 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r4-link1", + "from": "r4-c1", + "to": "r4-c2", + "points": [ + [ + 814, + 1031 + ], + [ + 846, + 1031 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r4-link2", + "from": "r4-c2", + "to": "r4-c3", + "points": [ + [ + 1262, + 1031 + ], + [ + 1294, + 1031 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r5-link0", + "from": "r5-c0", + "to": "r5-c1", + "points": [ + [ + 348, + 1219 + ], + [ + 380, + 1219 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r5-link1", + "from": "r5-c1", + "to": "r5-c2", + "points": [ + [ + 814, + 1219 + ], + [ + 846, + 1219 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "r5-link2", + "from": "r5-c2", + "to": "r5-c3", + "points": [ + [ + 1262, + 1219 + ], + [ + 1294, + 1219 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + } + ], + "apiReferences": [ + "CreateRepository", + "UpdateRepository", + "SyncRepository", + "UpsertRepositorySyncSchedule", + "BeginRepositoryUpload", + "PutRepositoryUploadChunk", + "CompleteRepositoryUpload", + "ListRepositoryQuarantine", + "PreviewRepositoryPackageInstall", + "ConfirmRepositoryPackageInstall", + "PreviewRepositoryPackageDeletion", + "ConfirmRepositoryPackageDeletion", + "DescribeRepositoryStorageNode", + "ConfigureRepositoryNodeInstallSource", + "CreateRepositoryNodeEnrollment", + "SyncRepository", + "ListRepositoryTasks", + "DescribeRepositoryTask", + "DownloadRepositoryTaskLog", + "CreateRepositoryOCAIAnalysis", + "GenerateRepositoryConfig", + "CreateRepositoryDistribution", + "PreviewRepositoryInventoryImport", + "ConfirmRepositoryInventoryImport" + ] +} diff --git a/docs/images/repository/repo-feature-map.svg b/docs/images/repository/repo-feature-map.svg new file mode 100644 index 0000000000000000000000000000000000000000..c94d86a7865d1d2f2f62a5199e009cb7f5314ab0 --- /dev/null +++ b/docs/images/repository/repo-feature-map.svg @@ -0,0 +1,162 @@ + + +从页面操作到后端执行 · 六条功能路径页面入口、管理协议、共享执行模块与可核验结果逐行对应,覆盖内容管理、节点运维和诊断。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +从页面操作到后端执行 · 六条功能路径 +页面入口、管理协议、共享执行模块与可核验结果逐行对应,覆盖内容管理、节点运维和诊断。 +前端页面 / 操作 +真实 Repository RPC +后端与执行模块 +可见结果 / 验证点 +01 +仓库管理 / 上游源 +创建、筛选同步、计划同步 +CreateRepository +UpdateRepository +SyncRepository +UpsertRepositorySyncSchedule +仓库 / 内容 / 调度服务 +原有任务 + Mutation Lock +Node Worker → 新 Revision +源地址与过滤策略可查 +新包、repodata 与发布状态 +MIRROR / APPEND 与实际限速 +02 +软件包 / 上传 Drawer +多文件、暂停恢复、隔离区 +BeginRepositoryUpload +PutRepositoryUploadChunk +CompleteRepositoryUpload +ListRepositoryQuarantine +内容服务 + 分片记录 +Node 接收、校验与合并 +RPM / GPG / 归档安全检查 +缺失分片补传、完整 SHA-256 +合法包发布;拒绝包隔离 +tar 部分接收为部分成功 +03 +软件包 / 安装与删除 +先预演,再确认执行 +PreviewRepositoryPackageInstall +ConfirmRepositoryPackageInstall +PreviewRepositoryPackageDeletion +ConfirmRepositoryPackageDeletion +预演摘要 + 不可变版本 +Task Target / Result Item +安装:现有 TMS + DNF +删除:Worker + 单次发布 +各目标独立安装结果 +直接 / 传递依赖影响可见 +批量删包只重建一次元数据 +04 +仓库节点 / 节点运维 +容量、同步、安装与白名单 +DescribeRepositoryStorageNode +ConfigureRepositoryNodeInstallSource +CreateRepositoryNodeEnrollment +SyncRepository +节点 / 预检 / 占用服务 +正式 Node RPM 源 + 现有 TMS +白名单 / Bootstrap / 证书 +逻辑大小与物理占用分开 +EVR 最新 / 历史版本可选 +标准源 DNF 安装后自动注册 +05 +任务总览 / Task Drawer +筛选、日志、诊断与 AI +ListRepositoryTasks +DescribeRepositoryTask +DownloadRepositoryTaskLog +CreateRepositoryOCAIAnalysis +任务服务 / 统一错误目录 +节点 JSONL.zst + 脱敏 Bundle +上下文服务 + 现有 OCAI SSE +四态统计与 Target 结果 +英文 Code + 中文原因 / 建议 +真实只读分析,不自动修复 +06 +仓库配置 / 分发 / 导入 +生成配置、目标分发、定义导入 +GenerateRepositoryConfig +CreateRepositoryDistribution +PreviewRepositoryInventoryImport +ConfirmRepositoryInventoryImport +配置生成 / 分发 / 导入服务 +Revision + manifest 绑定 +原有 TMS / 统一 Repository Task +复制 / 下载 .repo 后真实 DNF +目标校验快照并原子切换 +定义可复用;不导出 Secret +页面:manager/frontend/src/pages/repository;协议与实现:manager/backend/{protocol/repository, services/footstone}。RPC 细节以 Proto 为准。 + diff --git a/docs/images/repository/repo-publish-flow.arch.json b/docs/images/repository/repo-publish-flow.arch.json new file mode 100644 index 0000000000000000000000000000000000000000..6caf92b28eb9846b65133f24bd34511d3858dabb --- /dev/null +++ b/docs/images/repository/repo-publish-flow.arch.json @@ -0,0 +1,783 @@ +{ + "name": "repo-publish-flow", + "title": "从同步 / 上传到可用源 · 远程发布闭环", + "subtitle": "节点处理文件,Footstone 提交元数据;通过 Prepare / Switch / Confirm 协调发布,不跨文件系统假设原子事务。", + "width": 1760, + "height": 1480, + "shapes": [ + { + "id": "files", + "x": 40, + "y": 130, + "w": 800, + "h": 1170, + "fill": "#EFF8F3", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "files-accent", + "x": 40, + "y": 148, + "w": 4, + "h": 27, + "fill": "#15765C", + "stroke": "#15765C", + "r": 1 + }, + { + "id": "metadata", + "x": 920, + "y": 130, + "w": 800, + "h": 1170, + "fill": "#F1F6FF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "metadata-accent", + "x": 920, + "y": 148, + "w": 4, + "h": 27, + "fill": "#245EC7", + "stroke": "#245EC7", + "r": 1 + }, + { + "id": "claim", + "x": 80, + "y": 200, + "w": 720, + "h": 118, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "enqueue", + "x": 960, + "y": 200, + "w": 720, + "h": 118, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "staging", + "x": 80, + "y": 372, + "w": 720, + "h": 126, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "build", + "x": 80, + "y": 552, + "w": 720, + "h": 126, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "prepare", + "x": 960, + "y": 552, + "w": 720, + "h": 126, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "switch", + "x": 80, + "y": 780, + "w": 720, + "h": 126, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "confirm", + "x": 80, + "y": 1002, + "w": 720, + "h": 126, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "active", + "x": 960, + "y": 1002, + "w": 720, + "h": 126, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "lease-note", + "x": 960, + "y": 372, + "w": 720, + "h": 126, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "read", + "x": 80, + "y": 1180, + "w": 720, + "h": 96, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "recovery", + "x": 960, + "y": 1180, + "w": 720, + "h": 96, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "policies", + "x": 40, + "y": 1330, + "w": 1680, + "h": 94, + "fill": "#FFF7E9", + "stroke": "#D7E1ED", + "r": 8 + } + ], + "texts": [ + { + "x": 48, + "y": 55, + "value": "从同步 / 上传到可用源 · 远程发布闭环", + "size": 34, + "color": "#172B4D", + "weight": 700, + "maxWidth": 1672 + }, + { + "x": 48, + "y": 91, + "value": "节点处理文件,Footstone 提交元数据;通过 Prepare / Switch / Confirm 协调发布,不跨文件系统假设原子事务。", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 1672 + }, + { + "x": 64, + "y": 169, + "value": "仓库节点 · 文件与 current", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 752 + }, + { + "x": 944, + "y": 169, + "value": "Footstone · Task、锁与数据库事务", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 752 + }, + { + "x": 100, + "y": 234, + "value": "01 领取任务并复核预检", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 680 + }, + { + "x": 100, + "y": 266, + "value": "Worker 通过 mTLS 主动 ClaimTask / Heartbeat", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 100, + "y": 293, + "value": "磁盘、inode、工具、lease 与 fencing token 均有效", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 980, + "y": 234, + "value": "创建 Task,绑定节点 / Preflight", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 680 + }, + { + "x": 980, + "y": 266, + "value": "保存参数快照;同仓库 mutation 串行", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 980, + "y": 293, + "value": "先有任务身份,再允许节点执行", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 805, + "y": 242, + "value": "mTLS 领取 / 心跳", + "size": 17, + "color": "#245EC7", + "weight": 500, + "maxWidth": 915 + }, + { + "x": 100, + "y": 406, + "value": "02 staging 接收与校验", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 680 + }, + { + "x": 100, + "y": 438, + "value": "同步 snapshot 或合并 Indexed Chunks", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 100, + "y": 465, + "value": "RPM / 架构 / checksum / GPG / 归档安全", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 100, + "y": 492, + "value": "拒绝包 → quarantine;Sync 失败不发布", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 100, + "y": 586, + "value": "03 构建候选 immutable Revision", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 680 + }, + { + "x": 100, + "y": 618, + "value": "createrepo_c → 标准 repodata;按配置签元数据", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 100, + "y": 645, + "value": "固化 manifest、repomd 摘要与 result.jsonl.zst", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 100, + "y": 672, + "value": "此时尚未改变 current / active_revision_id", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 980, + "y": 586, + "value": "PrepareTaskCommit → PREPARED", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 680 + }, + { + "x": 980, + "y": 618, + "value": "mTLS 读取结果对象并验证 SHA-256", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 980, + "y": 645, + "value": "短事务校验任务归属、锁、取消和 fencing", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 980, + "y": 672, + "value": "导入 Revision / Package / Quarantine 元数据", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 817, + "y": 598, + "value": "Prepare 请求", + "size": 17, + "color": "#245EC7", + "weight": 500, + "maxWidth": 903 + }, + { + "x": 100, + "y": 814, + "value": "04 切换节点 current 并核验", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 680 + }, + { + "x": 100, + "y": 846, + "value": "收到 Prepare 确认后,在同文件系统执行", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 100, + "y": 873, + "value": "相对 symlink + rename → 原子切换 current", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 100, + "y": 900, + "value": "校验 current 目标与 repomd.xml", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 832, + "y": 717, + "value": "Prepare 确认", + "size": 17, + "color": "#245EC7", + "weight": 500, + "maxWidth": 888 + }, + { + "x": 100, + "y": 1036, + "value": "05 ConfirmTaskCommit", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 680 + }, + { + "x": 100, + "y": 1068, + "value": "提交 revision / manifest / commit token", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 100, + "y": 1095, + "value": "保持同一 Task、节点身份与 fencing token", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 100, + "y": 1122, + "value": "网络异常不能直接视为“已回滚”", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 980, + "y": 1036, + "value": "06 确认 active Revision 与任务终态", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 680 + }, + { + "x": 980, + "y": 1068, + "value": "事务核对 PREPARED 记录及 commit token", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 980, + "y": 1095, + "value": "更新 active_revision_id、Task 与 Mutation Lock", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 980, + "y": 1122, + "value": "控制台显示本次包清单、版本与发布结果", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 817, + "y": 1048, + "value": "Confirm 请求", + "size": 17, + "color": "#245EC7", + "weight": 500, + "maxWidth": 903 + }, + { + "x": 980, + "y": 406, + "value": "全过程:任务归属与取消可检查", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 680 + }, + { + "x": 980, + "y": 438, + "value": "执行中持续续约;过期 Worker 不能用旧 fencing 发布", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 980, + "y": 465, + "value": "不同仓库可并发;相同仓库 mutation 不并发", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 980, + "y": 492, + "value": "上传传输结束不等于发布任务已成功", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 100, + "y": 1214, + "value": "只读源可用", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 680 + }, + { + "x": 100, + "y": 1246, + "value": "业务客户端 GET / HEAD RPM、repodata 与公开公钥", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 822, + "y": 1142, + "value": "确认成功", + "size": 17, + "color": "#15765C", + "weight": 500, + "maxWidth": 898 + }, + { + "x": 980, + "y": 1214, + "value": "异常边界:切换后确认丢失", + "size": 23, + "color": "#A55F16", + "weight": 700, + "maxWidth": 680 + }, + { + "x": 980, + "y": 1246, + "value": "保留 PREPARED / current 事实,重试确认或执行对账", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 64, + "y": 1368, + "value": "发布策略:Sync 关键步骤失败 → 不发布半份同步结果;tar 上传可将合法包发布、拒绝项隔离并标部分成功。", + "size": 21, + "color": "#172B4D", + "weight": 500, + "maxWidth": 1620 + }, + { + "x": 64, + "y": 1402, + "value": "确认异常按已切换链接与 PREPARED 记录对账;日常读取使用 current,分发 / 安装 / 取证绑定 immutable Revision。", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 1620 + }, + { + "x": 48, + "y": 1454, + "value": "控制路径:repositoryworker/remote_worker.go + application/repository_node_internal.go;没有新增第二套发布或回滚协议。", + "size": 17, + "color": "#53657B", + "weight": 400, + "maxWidth": 1664 + } + ], + "edges": [ + { + "id": "claim-request", + "from": "claim", + "to": "enqueue", + "points": [ + [ + 800, + 259 + ], + [ + 960, + 259 + ] + ], + "color": "#245EC7", + "dashed": false, + "reverse": false + }, + { + "id": "receive-content", + "from": "claim", + "to": "staging", + "points": [ + [ + 440, + 318 + ], + [ + 440, + 372 + ] + ], + "color": "#15765C", + "dashed": false, + "reverse": false + }, + { + "id": "build-content", + "from": "staging", + "to": "build", + "points": [ + [ + 440, + 498 + ], + [ + 440, + 552 + ] + ], + "color": "#15765C", + "dashed": false, + "reverse": false + }, + { + "id": "prepare-request", + "from": "build", + "to": "prepare", + "points": [ + [ + 800, + 615 + ], + [ + 960, + 615 + ] + ], + "color": "#245EC7", + "dashed": false, + "reverse": false + }, + { + "id": "prepared-response", + "from": "prepare", + "to": "switch", + "points": [ + [ + 1320, + 678 + ], + [ + 1320, + 730 + ], + [ + 440, + 730 + ], + [ + 440, + 780 + ] + ], + "color": "#245EC7", + "dashed": false, + "reverse": false + }, + { + "id": "confirm-after-switch", + "from": "switch", + "to": "confirm", + "points": [ + [ + 440, + 906 + ], + [ + 440, + 1002 + ] + ], + "color": "#15765C", + "dashed": false, + "reverse": false + }, + { + "id": "confirm-request", + "from": "confirm", + "to": "active", + "points": [ + [ + 800, + 1065 + ], + [ + 960, + 1065 + ] + ], + "color": "#245EC7", + "dashed": false, + "reverse": false + }, + { + "id": "published-read", + "from": "active", + "to": "read", + "points": [ + [ + 1320, + 1128 + ], + [ + 1320, + 1152 + ], + [ + 440, + 1152 + ], + [ + 440, + 1180 + ] + ], + "color": "#15765C", + "dashed": false, + "reverse": false + } + ] +} diff --git a/docs/images/repository/repo-publish-flow.dot b/docs/images/repository/repo-publish-flow.dot new file mode 100644 index 0000000000000000000000000000000000000000..0b435c61c66a203a0bb3a6a943ea20b90a865de9 --- /dev/null +++ b/docs/images/repository/repo-publish-flow.dot @@ -0,0 +1,28 @@ +// Logical edge reference generated by scripts/render-repository-diagrams.mjs. +// The SVG / drawio files retain the reviewed fixed layout. +digraph repo_publish_flow { + graph [rankdir=LR, fontname="Microsoft YaHei"]; + node [shape=box, fontname="Microsoft YaHei"]; + "files" [label="仓库节点 · 文件与 current\n01 领取任务并复核预检\nWorker 通过 mTLS 主动 ClaimTask / Heartbeat\n磁盘、inode、工具、lease 与 fencing token 均有效"]; + "metadata" [label="Footstone · Task、锁与数据库事务\n创建 Task,绑定节点 / Preflight\n保存参数快照;同仓库 mutation 串行\n先有任务身份,再允许节点执行"]; + "claim" [label="01 领取任务并复核预检\nWorker 通过 mTLS 主动 ClaimTask / Heartbeat\n磁盘、inode、工具、lease 与 fencing token 均有效"]; + "enqueue" [label="创建 Task,绑定节点 / Preflight\n保存参数快照;同仓库 mutation 串行\n先有任务身份,再允许节点执行"]; + "staging" [label="02 staging 接收与校验\n同步 snapshot 或合并 Indexed Chunks\nRPM / 架构 / checksum / GPG / 归档安全\n拒绝包 → quarantine;Sync 失败不发布"]; + "build" [label="03 构建候选 immutable Revision\ncreaterepo_c → 标准 repodata;按配置签元数据\n固化 manifest、repomd 摘要与 result.jsonl.zst\n此时尚未改变 current / active_revision_id"]; + "prepare" [label="PrepareTaskCommit → PREPARED\nmTLS 读取结果对象并验证 SHA-256\n短事务校验任务归属、锁、取消和 fencing\n导入 Revision / Package / Quarantine 元数据"]; + "switch" [label="04 切换节点 current 并核验\n收到 Prepare 确认后,在同文件系统执行\n相对 symlink + rename → 原子切换 current\n校验 current 目标与 repomd.xml"]; + "confirm" [label="05 ConfirmTaskCommit\n提交 revision / manifest / commit token\n保持同一 Task、节点身份与 fencing token\n网络异常不能直接视为“已回滚”"]; + "active" [label="06 确认 active Revision 与任务终态\n事务核对 PREPARED 记录及 commit token\n更新 active_revision_id、Task 与 Mutation Lock\n控制台显示本次包清单、版本与发布结果"]; + "lease-note" [label="全过程:任务归属与取消可检查\n执行中持续续约;过期 Worker 不能用旧 fencing 发布\n不同仓库可并发;相同仓库 mutation 不并发\n上传传输结束不等于发布任务已成功"]; + "read" [label="只读源可用\n业务客户端 GET / HEAD RPM、repodata 与公开公钥"]; + "recovery" [label="异常边界:切换后确认丢失\n保留 PREPARED / current 事实,重试确认或执行对账"]; + "policies" [label="发布策略:Sync 关键步骤失败 → 不发布半份同步结果;tar 上传可将合法包发布、拒绝项隔离并标部分成功。\n确认异常按已切换链接与 PREPARED 记录对账;日常读取使用 current,分发 / 安装 / 取证绑定 immutable Revision。"]; + "claim" -> "enqueue" [color="#245EC7"]; + "claim" -> "staging" [color="#15765C"]; + "staging" -> "build" [color="#15765C"]; + "build" -> "prepare" [color="#245EC7"]; + "prepare" -> "switch" [color="#245EC7"]; + "switch" -> "confirm" [color="#15765C"]; + "confirm" -> "active" [color="#245EC7"]; + "active" -> "read" [color="#15765C"]; +} diff --git a/docs/images/repository/repo-publish-flow.drawio b/docs/images/repository/repo-publish-flow.drawio new file mode 100644 index 0000000000000000000000000000000000000000..a18f8473144b719414a44adfe06fd93f6caad4e0 --- /dev/null +++ b/docs/images/repository/repo-publish-flow.drawio @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/repository/repo-publish-flow.png b/docs/images/repository/repo-publish-flow.png new file mode 100644 index 0000000000000000000000000000000000000000..40ce85385328393fd81ee7f82a07aa40fd297c58 Binary files /dev/null and b/docs/images/repository/repo-publish-flow.png differ diff --git a/docs/images/repository/repo-publish-flow.spec.yaml b/docs/images/repository/repo-publish-flow.spec.yaml new file mode 100644 index 0000000000000000000000000000000000000000..6caf92b28eb9846b65133f24bd34511d3858dabb --- /dev/null +++ b/docs/images/repository/repo-publish-flow.spec.yaml @@ -0,0 +1,783 @@ +{ + "name": "repo-publish-flow", + "title": "从同步 / 上传到可用源 · 远程发布闭环", + "subtitle": "节点处理文件,Footstone 提交元数据;通过 Prepare / Switch / Confirm 协调发布,不跨文件系统假设原子事务。", + "width": 1760, + "height": 1480, + "shapes": [ + { + "id": "files", + "x": 40, + "y": 130, + "w": 800, + "h": 1170, + "fill": "#EFF8F3", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "files-accent", + "x": 40, + "y": 148, + "w": 4, + "h": 27, + "fill": "#15765C", + "stroke": "#15765C", + "r": 1 + }, + { + "id": "metadata", + "x": 920, + "y": 130, + "w": 800, + "h": 1170, + "fill": "#F1F6FF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "metadata-accent", + "x": 920, + "y": 148, + "w": 4, + "h": 27, + "fill": "#245EC7", + "stroke": "#245EC7", + "r": 1 + }, + { + "id": "claim", + "x": 80, + "y": 200, + "w": 720, + "h": 118, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "enqueue", + "x": 960, + "y": 200, + "w": 720, + "h": 118, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "staging", + "x": 80, + "y": 372, + "w": 720, + "h": 126, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "build", + "x": 80, + "y": 552, + "w": 720, + "h": 126, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "prepare", + "x": 960, + "y": 552, + "w": 720, + "h": 126, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "switch", + "x": 80, + "y": 780, + "w": 720, + "h": 126, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "confirm", + "x": 80, + "y": 1002, + "w": 720, + "h": 126, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "active", + "x": 960, + "y": 1002, + "w": 720, + "h": 126, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "lease-note", + "x": 960, + "y": 372, + "w": 720, + "h": 126, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "read", + "x": 80, + "y": 1180, + "w": 720, + "h": 96, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "recovery", + "x": 960, + "y": 1180, + "w": 720, + "h": 96, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "policies", + "x": 40, + "y": 1330, + "w": 1680, + "h": 94, + "fill": "#FFF7E9", + "stroke": "#D7E1ED", + "r": 8 + } + ], + "texts": [ + { + "x": 48, + "y": 55, + "value": "从同步 / 上传到可用源 · 远程发布闭环", + "size": 34, + "color": "#172B4D", + "weight": 700, + "maxWidth": 1672 + }, + { + "x": 48, + "y": 91, + "value": "节点处理文件,Footstone 提交元数据;通过 Prepare / Switch / Confirm 协调发布,不跨文件系统假设原子事务。", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 1672 + }, + { + "x": 64, + "y": 169, + "value": "仓库节点 · 文件与 current", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 752 + }, + { + "x": 944, + "y": 169, + "value": "Footstone · Task、锁与数据库事务", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 752 + }, + { + "x": 100, + "y": 234, + "value": "01 领取任务并复核预检", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 680 + }, + { + "x": 100, + "y": 266, + "value": "Worker 通过 mTLS 主动 ClaimTask / Heartbeat", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 100, + "y": 293, + "value": "磁盘、inode、工具、lease 与 fencing token 均有效", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 980, + "y": 234, + "value": "创建 Task,绑定节点 / Preflight", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 680 + }, + { + "x": 980, + "y": 266, + "value": "保存参数快照;同仓库 mutation 串行", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 980, + "y": 293, + "value": "先有任务身份,再允许节点执行", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 805, + "y": 242, + "value": "mTLS 领取 / 心跳", + "size": 17, + "color": "#245EC7", + "weight": 500, + "maxWidth": 915 + }, + { + "x": 100, + "y": 406, + "value": "02 staging 接收与校验", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 680 + }, + { + "x": 100, + "y": 438, + "value": "同步 snapshot 或合并 Indexed Chunks", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 100, + "y": 465, + "value": "RPM / 架构 / checksum / GPG / 归档安全", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 100, + "y": 492, + "value": "拒绝包 → quarantine;Sync 失败不发布", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 100, + "y": 586, + "value": "03 构建候选 immutable Revision", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 680 + }, + { + "x": 100, + "y": 618, + "value": "createrepo_c → 标准 repodata;按配置签元数据", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 100, + "y": 645, + "value": "固化 manifest、repomd 摘要与 result.jsonl.zst", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 100, + "y": 672, + "value": "此时尚未改变 current / active_revision_id", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 980, + "y": 586, + "value": "PrepareTaskCommit → PREPARED", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 680 + }, + { + "x": 980, + "y": 618, + "value": "mTLS 读取结果对象并验证 SHA-256", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 980, + "y": 645, + "value": "短事务校验任务归属、锁、取消和 fencing", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 980, + "y": 672, + "value": "导入 Revision / Package / Quarantine 元数据", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 817, + "y": 598, + "value": "Prepare 请求", + "size": 17, + "color": "#245EC7", + "weight": 500, + "maxWidth": 903 + }, + { + "x": 100, + "y": 814, + "value": "04 切换节点 current 并核验", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 680 + }, + { + "x": 100, + "y": 846, + "value": "收到 Prepare 确认后,在同文件系统执行", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 100, + "y": 873, + "value": "相对 symlink + rename → 原子切换 current", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 100, + "y": 900, + "value": "校验 current 目标与 repomd.xml", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 832, + "y": 717, + "value": "Prepare 确认", + "size": 17, + "color": "#245EC7", + "weight": 500, + "maxWidth": 888 + }, + { + "x": 100, + "y": 1036, + "value": "05 ConfirmTaskCommit", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 680 + }, + { + "x": 100, + "y": 1068, + "value": "提交 revision / manifest / commit token", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 100, + "y": 1095, + "value": "保持同一 Task、节点身份与 fencing token", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 100, + "y": 1122, + "value": "网络异常不能直接视为“已回滚”", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 980, + "y": 1036, + "value": "06 确认 active Revision 与任务终态", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 680 + }, + { + "x": 980, + "y": 1068, + "value": "事务核对 PREPARED 记录及 commit token", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 980, + "y": 1095, + "value": "更新 active_revision_id、Task 与 Mutation Lock", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 980, + "y": 1122, + "value": "控制台显示本次包清单、版本与发布结果", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 817, + "y": 1048, + "value": "Confirm 请求", + "size": 17, + "color": "#245EC7", + "weight": 500, + "maxWidth": 903 + }, + { + "x": 980, + "y": 406, + "value": "全过程:任务归属与取消可检查", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 680 + }, + { + "x": 980, + "y": 438, + "value": "执行中持续续约;过期 Worker 不能用旧 fencing 发布", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 980, + "y": 465, + "value": "不同仓库可并发;相同仓库 mutation 不并发", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 980, + "y": 492, + "value": "上传传输结束不等于发布任务已成功", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 100, + "y": 1214, + "value": "只读源可用", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 680 + }, + { + "x": 100, + "y": 1246, + "value": "业务客户端 GET / HEAD RPM、repodata 与公开公钥", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 822, + "y": 1142, + "value": "确认成功", + "size": 17, + "color": "#15765C", + "weight": 500, + "maxWidth": 898 + }, + { + "x": 980, + "y": 1214, + "value": "异常边界:切换后确认丢失", + "size": 23, + "color": "#A55F16", + "weight": 700, + "maxWidth": 680 + }, + { + "x": 980, + "y": 1246, + "value": "保留 PREPARED / current 事实,重试确认或执行对账", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 680 + }, + { + "x": 64, + "y": 1368, + "value": "发布策略:Sync 关键步骤失败 → 不发布半份同步结果;tar 上传可将合法包发布、拒绝项隔离并标部分成功。", + "size": 21, + "color": "#172B4D", + "weight": 500, + "maxWidth": 1620 + }, + { + "x": 64, + "y": 1402, + "value": "确认异常按已切换链接与 PREPARED 记录对账;日常读取使用 current,分发 / 安装 / 取证绑定 immutable Revision。", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 1620 + }, + { + "x": 48, + "y": 1454, + "value": "控制路径:repositoryworker/remote_worker.go + application/repository_node_internal.go;没有新增第二套发布或回滚协议。", + "size": 17, + "color": "#53657B", + "weight": 400, + "maxWidth": 1664 + } + ], + "edges": [ + { + "id": "claim-request", + "from": "claim", + "to": "enqueue", + "points": [ + [ + 800, + 259 + ], + [ + 960, + 259 + ] + ], + "color": "#245EC7", + "dashed": false, + "reverse": false + }, + { + "id": "receive-content", + "from": "claim", + "to": "staging", + "points": [ + [ + 440, + 318 + ], + [ + 440, + 372 + ] + ], + "color": "#15765C", + "dashed": false, + "reverse": false + }, + { + "id": "build-content", + "from": "staging", + "to": "build", + "points": [ + [ + 440, + 498 + ], + [ + 440, + 552 + ] + ], + "color": "#15765C", + "dashed": false, + "reverse": false + }, + { + "id": "prepare-request", + "from": "build", + "to": "prepare", + "points": [ + [ + 800, + 615 + ], + [ + 960, + 615 + ] + ], + "color": "#245EC7", + "dashed": false, + "reverse": false + }, + { + "id": "prepared-response", + "from": "prepare", + "to": "switch", + "points": [ + [ + 1320, + 678 + ], + [ + 1320, + 730 + ], + [ + 440, + 730 + ], + [ + 440, + 780 + ] + ], + "color": "#245EC7", + "dashed": false, + "reverse": false + }, + { + "id": "confirm-after-switch", + "from": "switch", + "to": "confirm", + "points": [ + [ + 440, + 906 + ], + [ + 440, + 1002 + ] + ], + "color": "#15765C", + "dashed": false, + "reverse": false + }, + { + "id": "confirm-request", + "from": "confirm", + "to": "active", + "points": [ + [ + 800, + 1065 + ], + [ + 960, + 1065 + ] + ], + "color": "#245EC7", + "dashed": false, + "reverse": false + }, + { + "id": "published-read", + "from": "active", + "to": "read", + "points": [ + [ + 1320, + 1128 + ], + [ + 1320, + 1152 + ], + [ + 440, + 1152 + ], + [ + 440, + 1180 + ] + ], + "color": "#15765C", + "dashed": false, + "reverse": false + } + ] +} diff --git a/docs/images/repository/repo-publish-flow.svg b/docs/images/repository/repo-publish-flow.svg new file mode 100644 index 0000000000000000000000000000000000000000..8abdbaf9c8c5ba15fc4caa1c016ec6433dd32f74 --- /dev/null +++ b/docs/images/repository/repo-publish-flow.svg @@ -0,0 +1,80 @@ + + +从同步 / 上传到可用源 · 远程发布闭环节点处理文件,Footstone 提交元数据;通过 Prepare / Switch / Confirm 协调发布,不跨文件系统假设原子事务。 + + + + + + + + + + + + + + + + + + + + + + + + + + +从同步 / 上传到可用源 · 远程发布闭环 +节点处理文件,Footstone 提交元数据;通过 Prepare / Switch / Confirm 协调发布,不跨文件系统假设原子事务。 +仓库节点 · 文件与 current +Footstone · Task、锁与数据库事务 +01 领取任务并复核预检 +Worker 通过 mTLS 主动 ClaimTask / Heartbeat +磁盘、inode、工具、lease 与 fencing token 均有效 +创建 Task,绑定节点 / Preflight +保存参数快照;同仓库 mutation 串行 +先有任务身份,再允许节点执行 +mTLS 领取 / 心跳 +02 staging 接收与校验 +同步 snapshot 或合并 Indexed Chunks +RPM / 架构 / checksum / GPG / 归档安全 +拒绝包 → quarantine;Sync 失败不发布 +03 构建候选 immutable Revision +createrepo_c → 标准 repodata;按配置签元数据 +固化 manifest、repomd 摘要与 result.jsonl.zst +此时尚未改变 current / active_revision_id +PrepareTaskCommit → PREPARED +mTLS 读取结果对象并验证 SHA-256 +短事务校验任务归属、锁、取消和 fencing +导入 Revision / Package / Quarantine 元数据 +Prepare 请求 +04 切换节点 current 并核验 +收到 Prepare 确认后,在同文件系统执行 +相对 symlink + rename → 原子切换 current +校验 current 目标与 repomd.xml +Prepare 确认 +05 ConfirmTaskCommit +提交 revision / manifest / commit token +保持同一 Task、节点身份与 fencing token +网络异常不能直接视为“已回滚” +06 确认 active Revision 与任务终态 +事务核对 PREPARED 记录及 commit token +更新 active_revision_id、Task 与 Mutation Lock +控制台显示本次包清单、版本与发布结果 +Confirm 请求 +全过程:任务归属与取消可检查 +执行中持续续约;过期 Worker 不能用旧 fencing 发布 +不同仓库可并发;相同仓库 mutation 不并发 +上传传输结束不等于发布任务已成功 +只读源可用 +业务客户端 GET / HEAD RPM、repodata 与公开公钥 +确认成功 +异常边界:切换后确认丢失 +保留 PREPARED / current 事实,重试确认或执行对账 +发布策略:Sync 关键步骤失败 → 不发布半份同步结果;tar 上传可将合法包发布、拒绝项隔离并标部分成功。 +确认异常按已切换链接与 PREPARED 记录对账;日常读取使用 current,分发 / 安装 / 取证绑定 immutable Revision。 +控制路径:repositoryworker/remote_worker.go + application/repository_node_internal.go;没有新增第二套发布或回滚协议。 + diff --git a/docs/images/repository/repo-task-state.arch.json b/docs/images/repository/repo-task-state.arch.json new file mode 100644 index 0000000000000000000000000000000000000000..003e181501896c24f1c856e6cf99d303caaeec51 --- /dev/null +++ b/docs/images/repository/repo-task-state.arch.json @@ -0,0 +1,615 @@ +{ + "name": "repo-task-state", + "title": "统一任务状态 · 取消、重试与结果边界", + "subtitle": "整体 Task 描述业务操作;节点执行结果和逐包结果保留在同一任务下,统一追踪批量操作。", + "width": 1760, + "height": 1300, + "shapes": [ + { + "id": "pending", + "x": 704, + "y": 166, + "w": 352, + "h": 100, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "running", + "x": 704, + "y": 370, + "w": 352, + "h": 100, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "cancel-request", + "x": 1268, + "y": 370, + "w": 416, + "h": 100, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "canceled", + "x": 1268, + "y": 532, + "w": 416, + "h": 100, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "success", + "x": 64, + "y": 760, + "w": 456, + "h": 128, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "partial", + "x": 652, + "y": 760, + "w": 456, + "h": 128, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "failed", + "x": 1240, + "y": 760, + "w": 456, + "h": 128, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "boundaries", + "x": 40, + "y": 1010, + "w": 1680, + "h": 222, + "fill": "#F7F9FC", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "boundaries-accent", + "x": 40, + "y": 1028, + "w": 4, + "h": 27, + "fill": "#172B4D", + "stroke": "#172B4D", + "r": 1 + } + ], + "texts": [ + { + "x": 48, + "y": 55, + "value": "统一任务状态 · 取消、重试与结果边界", + "size": 34, + "color": "#172B4D", + "weight": 700, + "maxWidth": 1672 + }, + { + "x": 48, + "y": 91, + "value": "整体 Task 描述业务操作;节点执行结果和逐包结果保留在同一任务下,统一追踪批量操作。", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 1672 + }, + { + "x": 724, + "y": 200, + "value": "等待执行 PENDING", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 312 + }, + { + "x": 724, + "y": 232, + "value": "尚未领取或等待重新领取", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 312 + }, + { + "x": 724, + "y": 404, + "value": "运行中 RUNNING", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 312 + }, + { + "x": 724, + "y": 436, + "value": "有效 lease + fencing token", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 312 + }, + { + "x": 905, + "y": 325, + "value": "领取任务 / 获取锁", + "size": 17, + "color": "#245EC7", + "weight": 500, + "maxWidth": 815 + }, + { + "x": 1288, + "y": 404, + "value": "请求取消 CANCEL_REQUESTED", + "size": 23, + "color": "#A55F16", + "weight": 700, + "maxWidth": 376 + }, + { + "x": 1288, + "y": 436, + "value": "运行中取消先发送信号", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 376 + }, + { + "x": 1288, + "y": 566, + "value": "已取消 CANCELED", + "size": 23, + "color": "#53657B", + "weight": 700, + "maxWidth": 376 + }, + { + "x": 1288, + "y": 598, + "value": "Worker 确认或等待任务直接取消", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 376 + }, + { + "x": 1115, + "y": 402, + "value": "用户取消", + "size": 17, + "color": "#A55F16", + "weight": 500, + "maxWidth": 605 + }, + { + "x": 1490, + "y": 510, + "value": "Worker 确认", + "size": 17, + "color": "#A55F16", + "weight": 500, + "maxWidth": 230 + }, + { + "x": 1310, + "y": 197, + "value": "PENDING 可直接取消", + "size": 17, + "color": "#8B9BB0", + "weight": 500, + "maxWidth": 410 + }, + { + "x": 84, + "y": 794, + "value": "成功 SUCCESS", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 416 + }, + { + "x": 84, + "y": 826, + "value": "所有关键步骤通过", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 416 + }, + { + "x": 84, + "y": 853, + "value": "涉及发布时,版本与任务均已确认", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 416 + }, + { + "x": 672, + "y": 794, + "value": "部分成功 PARTIAL_SUCCESS", + "size": 23, + "color": "#A55F16", + "weight": 700, + "maxWidth": 416 + }, + { + "x": 672, + "y": 826, + "value": "tar 摄取部分接受 / 多目标部分成功", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 416 + }, + { + "x": 672, + "y": 853, + "value": "适用于 UPLOAD / DISTRIBUTE", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 416 + }, + { + "x": 672, + "y": 880, + "value": "以及 PACKAGE_INSTALL", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 416 + }, + { + "x": 1260, + "y": 794, + "value": "失败 FAILED", + "size": 23, + "color": "#B33D46", + "weight": 700, + "maxWidth": 416 + }, + { + "x": 1260, + "y": 826, + "value": "关键步骤失败或尝试次数耗尽", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 416 + }, + { + "x": 1260, + "y": 853, + "value": "保留 Error Code、日志和诊断", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 416 + }, + { + "x": 304, + "y": 727, + "value": "关键步骤通过", + "size": 17, + "color": "#15765C", + "weight": 500, + "maxWidth": 1416 + }, + { + "x": 899, + "y": 706, + "value": "允许部分结果的任务", + "size": 17, + "color": "#A55F16", + "weight": 500, + "maxWidth": 821 + }, + { + "x": 1482, + "y": 725, + "value": "执行失败", + "size": 17, + "color": "#B33D46", + "weight": 500, + "maxWidth": 238 + }, + { + "x": 314, + "y": 346, + "value": "lease 过期:原 Task 重领", + "size": 17, + "color": "#245EC7", + "weight": 500, + "maxWidth": 1406 + }, + { + "x": 760, + "y": 938, + "value": "人工 Retry:新 Task 关联旧 Task", + "size": 17, + "color": "#798BA4", + "weight": 500, + "maxWidth": 960 + }, + { + "x": 64, + "y": 1049, + "value": "任务整体与目标 / 逐包结果", + "size": 23, + "color": "#172B4D", + "weight": 700, + "maxWidth": 1632 + }, + { + "x": 64, + "y": 1092, + "value": "Task:整体状态、进度、开始 / 结束、错误与重试关系;Target:Node / Agent 独立执行结果;Result Item:逐包结果。", + "size": 21, + "color": "#172B4D", + "weight": 500, + "maxWidth": 1610 + }, + { + "x": 64, + "y": 1137, + "value": "Sync 保持完整快照语义,不发布部分结果;等待任务可直接取消,运行中任务由 Worker 确认取消。", + "size": 21, + "color": "#172B4D", + "weight": 500, + "maxWidth": 1610 + }, + { + "x": 64, + "y": 1182, + "value": "lease 恢复在原 Task 上增加 attempt;人工重试创建新 Task。所有影响 active 的变更仍需经过 Mutation Lock。", + "size": 21, + "color": "#172B4D", + "weight": 500, + "maxWidth": 1610 + }, + { + "x": 48, + "y": 1274, + "value": "状态以 RepositoryTaskService 及当前协议为准;Prepare 发布阶段的 PARTIAL_SUCCESS 仅用于允许部分接收的上传。", + "size": 17, + "color": "#53657B", + "weight": 400, + "maxWidth": 1664 + } + ], + "edges": [ + { + "id": "claim-task", + "from": "pending", + "to": "running", + "points": [ + [ + 880, + 266 + ], + [ + 880, + 370 + ] + ], + "color": "#245EC7", + "dashed": false, + "reverse": false + }, + { + "id": "running-cancel", + "from": "running", + "to": "cancel-request", + "points": [ + [ + 1056, + 420 + ], + [ + 1268, + 420 + ] + ], + "color": "#A55F16", + "dashed": false, + "reverse": false + }, + { + "id": "worker-cancel-ack", + "from": "cancel-request", + "to": "canceled", + "points": [ + [ + 1476, + 470 + ], + [ + 1476, + 532 + ] + ], + "color": "#A55F16", + "dashed": false, + "reverse": false + }, + { + "id": "pending-cancel", + "from": "pending", + "to": "canceled", + "points": [ + [ + 1056, + 216 + ], + [ + 1712, + 216 + ], + [ + 1712, + 582 + ], + [ + 1684, + 582 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "run-success", + "from": "running", + "to": "success", + "points": [ + [ + 828, + 470 + ], + [ + 828, + 672 + ], + [ + 292, + 672 + ], + [ + 292, + 760 + ] + ], + "color": "#15765C", + "dashed": false, + "reverse": false + }, + { + "id": "run-partial", + "from": "running", + "to": "partial", + "points": [ + [ + 880, + 470 + ], + [ + 880, + 760 + ] + ], + "color": "#A55F16", + "dashed": false, + "reverse": false + }, + { + "id": "run-fail", + "from": "running", + "to": "failed", + "points": [ + [ + 932, + 470 + ], + [ + 932, + 672 + ], + [ + 1468, + 672 + ], + [ + 1468, + 760 + ] + ], + "color": "#B33D46", + "dashed": false, + "reverse": false + }, + { + "id": "lease-reclaim", + "from": "running", + "to": "pending", + "points": [ + [ + 704, + 420 + ], + [ + 610, + 420 + ], + [ + 610, + 216 + ], + [ + 704, + 216 + ] + ], + "color": "#245EC7", + "dashed": true, + "reverse": false + }, + { + "id": "manual-retry", + "from": "failed", + "to": "pending", + "points": [ + [ + 1468, + 888 + ], + [ + 1468, + 954 + ], + [ + 24, + 954 + ], + [ + 24, + 198 + ], + [ + 704, + 198 + ] + ], + "color": "#798BA4", + "dashed": true, + "reverse": false + } + ] +} diff --git a/docs/images/repository/repo-task-state.dot b/docs/images/repository/repo-task-state.dot new file mode 100644 index 0000000000000000000000000000000000000000..073f100afe1119012297a07fbd29380eed0682b0 --- /dev/null +++ b/docs/images/repository/repo-task-state.dot @@ -0,0 +1,23 @@ +// Logical edge reference generated by scripts/render-repository-diagrams.mjs. +// The SVG / drawio files retain the reviewed fixed layout. +digraph repo_task_state { + graph [rankdir=LR, fontname="Microsoft YaHei"]; + node [shape=box, fontname="Microsoft YaHei"]; + "pending" [label="等待执行 PENDING\n尚未领取或等待重新领取"]; + "running" [label="运行中 RUNNING\n有效 lease + fencing token"]; + "cancel-request" [label="请求取消 CANCEL_REQUESTED\n运行中取消先发送信号"]; + "canceled" [label="已取消 CANCELED\nWorker 确认或等待任务直接取消"]; + "success" [label="成功 SUCCESS\n所有关键步骤通过\n涉及发布时,版本与任务均已确认"]; + "partial" [label="部分成功 PARTIAL_SUCCESS\ntar 摄取部分接受 / 多目标部分成功\n适用于 UPLOAD / DISTRIBUTE\n以及 PACKAGE_INSTALL"]; + "failed" [label="失败 FAILED\n关键步骤失败或尝试次数耗尽\n保留 Error Code、日志和诊断"]; + "boundaries" [label="任务整体与目标 / 逐包结果\nTask:整体状态、进度、开始 / 结束、错误与重试关系;Target:Node / Agent 独立执行结果;Result Item:逐包结果。\nSync 保持完整快照语义,不发布部分结果;等待任务可直接取消,运行中任务由 Worker 确认取消。\nlease 恢复在原 Task 上增加 attempt;人工重试创建新 Task。所有影响 active 的变更仍需经过 Mutation Lock。"]; + "pending" -> "running" [color="#245EC7"]; + "running" -> "cancel-request" [color="#A55F16"]; + "cancel-request" -> "canceled" [color="#A55F16"]; + "pending" -> "canceled" [color="#8B9BB0"]; + "running" -> "success" [color="#15765C"]; + "running" -> "partial" [color="#A55F16"]; + "running" -> "failed" [color="#B33D46"]; + "running" -> "pending" [color="#245EC7",style=dashed]; + "failed" -> "pending" [color="#798BA4",style=dashed]; +} diff --git a/docs/images/repository/repo-task-state.drawio b/docs/images/repository/repo-task-state.drawio new file mode 100644 index 0000000000000000000000000000000000000000..701ac9105f5cd61b43503417949145a29425e0a2 --- /dev/null +++ b/docs/images/repository/repo-task-state.drawio @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/repository/repo-task-state.png b/docs/images/repository/repo-task-state.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed165571db0fead7c9b27dd1cfcec609f63cee8 Binary files /dev/null and b/docs/images/repository/repo-task-state.png differ diff --git a/docs/images/repository/repo-task-state.spec.yaml b/docs/images/repository/repo-task-state.spec.yaml new file mode 100644 index 0000000000000000000000000000000000000000..003e181501896c24f1c856e6cf99d303caaeec51 --- /dev/null +++ b/docs/images/repository/repo-task-state.spec.yaml @@ -0,0 +1,615 @@ +{ + "name": "repo-task-state", + "title": "统一任务状态 · 取消、重试与结果边界", + "subtitle": "整体 Task 描述业务操作;节点执行结果和逐包结果保留在同一任务下,统一追踪批量操作。", + "width": 1760, + "height": 1300, + "shapes": [ + { + "id": "pending", + "x": 704, + "y": 166, + "w": 352, + "h": 100, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "running", + "x": 704, + "y": 370, + "w": 352, + "h": 100, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "cancel-request", + "x": 1268, + "y": 370, + "w": 416, + "h": 100, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "canceled", + "x": 1268, + "y": 532, + "w": 416, + "h": 100, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "success", + "x": 64, + "y": 760, + "w": 456, + "h": 128, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "partial", + "x": 652, + "y": 760, + "w": 456, + "h": 128, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "failed", + "x": 1240, + "y": 760, + "w": 456, + "h": 128, + "fill": "#FFFFFF", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "boundaries", + "x": 40, + "y": 1010, + "w": 1680, + "h": 222, + "fill": "#F7F9FC", + "stroke": "#D7E1ED", + "r": 8 + }, + { + "id": "boundaries-accent", + "x": 40, + "y": 1028, + "w": 4, + "h": 27, + "fill": "#172B4D", + "stroke": "#172B4D", + "r": 1 + } + ], + "texts": [ + { + "x": 48, + "y": 55, + "value": "统一任务状态 · 取消、重试与结果边界", + "size": 34, + "color": "#172B4D", + "weight": 700, + "maxWidth": 1672 + }, + { + "x": 48, + "y": 91, + "value": "整体 Task 描述业务操作;节点执行结果和逐包结果保留在同一任务下,统一追踪批量操作。", + "size": 19, + "color": "#53657B", + "weight": 400, + "maxWidth": 1672 + }, + { + "x": 724, + "y": 200, + "value": "等待执行 PENDING", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 312 + }, + { + "x": 724, + "y": 232, + "value": "尚未领取或等待重新领取", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 312 + }, + { + "x": 724, + "y": 404, + "value": "运行中 RUNNING", + "size": 23, + "color": "#245EC7", + "weight": 700, + "maxWidth": 312 + }, + { + "x": 724, + "y": 436, + "value": "有效 lease + fencing token", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 312 + }, + { + "x": 905, + "y": 325, + "value": "领取任务 / 获取锁", + "size": 17, + "color": "#245EC7", + "weight": 500, + "maxWidth": 815 + }, + { + "x": 1288, + "y": 404, + "value": "请求取消 CANCEL_REQUESTED", + "size": 23, + "color": "#A55F16", + "weight": 700, + "maxWidth": 376 + }, + { + "x": 1288, + "y": 436, + "value": "运行中取消先发送信号", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 376 + }, + { + "x": 1288, + "y": 566, + "value": "已取消 CANCELED", + "size": 23, + "color": "#53657B", + "weight": 700, + "maxWidth": 376 + }, + { + "x": 1288, + "y": 598, + "value": "Worker 确认或等待任务直接取消", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 376 + }, + { + "x": 1115, + "y": 402, + "value": "用户取消", + "size": 17, + "color": "#A55F16", + "weight": 500, + "maxWidth": 605 + }, + { + "x": 1490, + "y": 510, + "value": "Worker 确认", + "size": 17, + "color": "#A55F16", + "weight": 500, + "maxWidth": 230 + }, + { + "x": 1310, + "y": 197, + "value": "PENDING 可直接取消", + "size": 17, + "color": "#8B9BB0", + "weight": 500, + "maxWidth": 410 + }, + { + "x": 84, + "y": 794, + "value": "成功 SUCCESS", + "size": 23, + "color": "#15765C", + "weight": 700, + "maxWidth": 416 + }, + { + "x": 84, + "y": 826, + "value": "所有关键步骤通过", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 416 + }, + { + "x": 84, + "y": 853, + "value": "涉及发布时,版本与任务均已确认", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 416 + }, + { + "x": 672, + "y": 794, + "value": "部分成功 PARTIAL_SUCCESS", + "size": 23, + "color": "#A55F16", + "weight": 700, + "maxWidth": 416 + }, + { + "x": 672, + "y": 826, + "value": "tar 摄取部分接受 / 多目标部分成功", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 416 + }, + { + "x": 672, + "y": 853, + "value": "适用于 UPLOAD / DISTRIBUTE", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 416 + }, + { + "x": 672, + "y": 880, + "value": "以及 PACKAGE_INSTALL", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 416 + }, + { + "x": 1260, + "y": 794, + "value": "失败 FAILED", + "size": 23, + "color": "#B33D46", + "weight": 700, + "maxWidth": 416 + }, + { + "x": 1260, + "y": 826, + "value": "关键步骤失败或尝试次数耗尽", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 416 + }, + { + "x": 1260, + "y": 853, + "value": "保留 Error Code、日志和诊断", + "size": 18, + "color": "#53657B", + "weight": 400, + "maxWidth": 416 + }, + { + "x": 304, + "y": 727, + "value": "关键步骤通过", + "size": 17, + "color": "#15765C", + "weight": 500, + "maxWidth": 1416 + }, + { + "x": 899, + "y": 706, + "value": "允许部分结果的任务", + "size": 17, + "color": "#A55F16", + "weight": 500, + "maxWidth": 821 + }, + { + "x": 1482, + "y": 725, + "value": "执行失败", + "size": 17, + "color": "#B33D46", + "weight": 500, + "maxWidth": 238 + }, + { + "x": 314, + "y": 346, + "value": "lease 过期:原 Task 重领", + "size": 17, + "color": "#245EC7", + "weight": 500, + "maxWidth": 1406 + }, + { + "x": 760, + "y": 938, + "value": "人工 Retry:新 Task 关联旧 Task", + "size": 17, + "color": "#798BA4", + "weight": 500, + "maxWidth": 960 + }, + { + "x": 64, + "y": 1049, + "value": "任务整体与目标 / 逐包结果", + "size": 23, + "color": "#172B4D", + "weight": 700, + "maxWidth": 1632 + }, + { + "x": 64, + "y": 1092, + "value": "Task:整体状态、进度、开始 / 结束、错误与重试关系;Target:Node / Agent 独立执行结果;Result Item:逐包结果。", + "size": 21, + "color": "#172B4D", + "weight": 500, + "maxWidth": 1610 + }, + { + "x": 64, + "y": 1137, + "value": "Sync 保持完整快照语义,不发布部分结果;等待任务可直接取消,运行中任务由 Worker 确认取消。", + "size": 21, + "color": "#172B4D", + "weight": 500, + "maxWidth": 1610 + }, + { + "x": 64, + "y": 1182, + "value": "lease 恢复在原 Task 上增加 attempt;人工重试创建新 Task。所有影响 active 的变更仍需经过 Mutation Lock。", + "size": 21, + "color": "#172B4D", + "weight": 500, + "maxWidth": 1610 + }, + { + "x": 48, + "y": 1274, + "value": "状态以 RepositoryTaskService 及当前协议为准;Prepare 发布阶段的 PARTIAL_SUCCESS 仅用于允许部分接收的上传。", + "size": 17, + "color": "#53657B", + "weight": 400, + "maxWidth": 1664 + } + ], + "edges": [ + { + "id": "claim-task", + "from": "pending", + "to": "running", + "points": [ + [ + 880, + 266 + ], + [ + 880, + 370 + ] + ], + "color": "#245EC7", + "dashed": false, + "reverse": false + }, + { + "id": "running-cancel", + "from": "running", + "to": "cancel-request", + "points": [ + [ + 1056, + 420 + ], + [ + 1268, + 420 + ] + ], + "color": "#A55F16", + "dashed": false, + "reverse": false + }, + { + "id": "worker-cancel-ack", + "from": "cancel-request", + "to": "canceled", + "points": [ + [ + 1476, + 470 + ], + [ + 1476, + 532 + ] + ], + "color": "#A55F16", + "dashed": false, + "reverse": false + }, + { + "id": "pending-cancel", + "from": "pending", + "to": "canceled", + "points": [ + [ + 1056, + 216 + ], + [ + 1712, + 216 + ], + [ + 1712, + 582 + ], + [ + 1684, + 582 + ] + ], + "color": "#8B9BB0", + "dashed": false, + "reverse": false + }, + { + "id": "run-success", + "from": "running", + "to": "success", + "points": [ + [ + 828, + 470 + ], + [ + 828, + 672 + ], + [ + 292, + 672 + ], + [ + 292, + 760 + ] + ], + "color": "#15765C", + "dashed": false, + "reverse": false + }, + { + "id": "run-partial", + "from": "running", + "to": "partial", + "points": [ + [ + 880, + 470 + ], + [ + 880, + 760 + ] + ], + "color": "#A55F16", + "dashed": false, + "reverse": false + }, + { + "id": "run-fail", + "from": "running", + "to": "failed", + "points": [ + [ + 932, + 470 + ], + [ + 932, + 672 + ], + [ + 1468, + 672 + ], + [ + 1468, + 760 + ] + ], + "color": "#B33D46", + "dashed": false, + "reverse": false + }, + { + "id": "lease-reclaim", + "from": "running", + "to": "pending", + "points": [ + [ + 704, + 420 + ], + [ + 610, + 420 + ], + [ + 610, + 216 + ], + [ + 704, + 216 + ] + ], + "color": "#245EC7", + "dashed": true, + "reverse": false + }, + { + "id": "manual-retry", + "from": "failed", + "to": "pending", + "points": [ + [ + 1468, + 888 + ], + [ + 1468, + 954 + ], + [ + 24, + 954 + ], + [ + 24, + 198 + ], + [ + 704, + 198 + ] + ], + "color": "#798BA4", + "dashed": true, + "reverse": false + } + ] +} diff --git a/docs/images/repository/repo-task-state.svg b/docs/images/repository/repo-task-state.svg new file mode 100644 index 0000000000000000000000000000000000000000..ce92276f0f91e090b3faa4c797b115b987996e2e --- /dev/null +++ b/docs/images/repository/repo-task-state.svg @@ -0,0 +1,58 @@ + + +统一任务状态 · 取消、重试与结果边界整体 Task 描述业务操作;节点执行结果和逐包结果保留在同一任务下,统一追踪批量操作。 + + + + + + + + + + + + + + + + + + + + +统一任务状态 · 取消、重试与结果边界 +整体 Task 描述业务操作;节点执行结果和逐包结果保留在同一任务下,统一追踪批量操作。 +等待执行 PENDING +尚未领取或等待重新领取 +运行中 RUNNING +有效 lease + fencing token +领取任务 / 获取锁 +请求取消 CANCEL_REQUESTED +运行中取消先发送信号 +已取消 CANCELED +Worker 确认或等待任务直接取消 +用户取消 +Worker 确认 +PENDING 可直接取消 +成功 SUCCESS +所有关键步骤通过 +涉及发布时,版本与任务均已确认 +部分成功 PARTIAL_SUCCESS +tar 摄取部分接受 / 多目标部分成功 +适用于 UPLOAD / DISTRIBUTE +以及 PACKAGE_INSTALL +失败 FAILED +关键步骤失败或尝试次数耗尽 +保留 Error Code、日志和诊断 +关键步骤通过 +允许部分结果的任务 +执行失败 +lease 过期:原 Task 重领 +人工 Retry:新 Task 关联旧 Task +任务整体与目标 / 逐包结果 +Task:整体状态、进度、开始 / 结束、错误与重试关系;Target:Node / Agent 独立执行结果;Result Item:逐包结果。 +Sync 保持完整快照语义,不发布部分结果;等待任务可直接取消,运行中任务由 Worker 确认取消。 +lease 恢复在原 Task 上增加 attempt;人工重试创建新 Task。所有影响 active 的变更仍需经过 Mutation Lock。 +状态以 RepositoryTaskService 及当前协议为准;Prepare 发布阶段的 PARTIAL_SUCCESS 仅用于允许部分接收的上传。 + diff --git a/docs/images/repository/repository-batch20-distribution-detail.png b/docs/images/repository/repository-batch20-distribution-detail.png new file mode 100644 index 0000000000000000000000000000000000000000..03dfc9b101742faeaac0fa301e85757f6c417889 Binary files /dev/null and b/docs/images/repository/repository-batch20-distribution-detail.png differ diff --git a/docs/images/repository/repository-batch20-distribution.png b/docs/images/repository/repository-batch20-distribution.png new file mode 100644 index 0000000000000000000000000000000000000000..76b96453ad834e90016993e2e5a1c506f474243a Binary files /dev/null and b/docs/images/repository/repository-batch20-distribution.png differ diff --git a/docs/images/repository/repository-batch20-overview.png b/docs/images/repository/repository-batch20-overview.png new file mode 100644 index 0000000000000000000000000000000000000000..661c5a4ddbcb1f36fb6b54af77f131f4adc0c12a Binary files /dev/null and b/docs/images/repository/repository-batch20-overview.png differ diff --git a/docs/images/repository/repository-batch20-packages.png b/docs/images/repository/repository-batch20-packages.png new file mode 100644 index 0000000000000000000000000000000000000000..d3c07d0846379ecdcea3c26c9cffc5b18755c521 Binary files /dev/null and b/docs/images/repository/repository-batch20-packages.png differ diff --git a/docs/images/repository/repository-build105-create-source-filters.png b/docs/images/repository/repository-build105-create-source-filters.png new file mode 100644 index 0000000000000000000000000000000000000000..678260dfa1b69f8c0fe4cdb3c1cd56b7dda4bf80 Binary files /dev/null and b/docs/images/repository/repository-build105-create-source-filters.png differ diff --git a/docs/images/repository/repository-build105-create-source-profile.png b/docs/images/repository/repository-build105-create-source-profile.png new file mode 100644 index 0000000000000000000000000000000000000000..fdb60943b96416b0204520cb1b13f02b73e5dca2 Binary files /dev/null and b/docs/images/repository/repository-build105-create-source-profile.png differ diff --git a/docs/images/repository/repository-build105-list.png b/docs/images/repository/repository-build105-list.png new file mode 100644 index 0000000000000000000000000000000000000000..59e38e6dcc8a302e0bee98d6d4d5222f38ede556 Binary files /dev/null and b/docs/images/repository/repository-build105-list.png differ diff --git a/docs/images/repository/repository-build105-ocai-context.png b/docs/images/repository/repository-build105-ocai-context.png new file mode 100644 index 0000000000000000000000000000000000000000..5cd61fd7c373871811e481f368bfda96d6067777 Binary files /dev/null and b/docs/images/repository/repository-build105-ocai-context.png differ diff --git a/docs/images/repository/repository-build105-ocai-diagnosis.png b/docs/images/repository/repository-build105-ocai-diagnosis.png new file mode 100644 index 0000000000000000000000000000000000000000..91010d77866c7b690a3938d68a46819d4a0359fe Binary files /dev/null and b/docs/images/repository/repository-build105-ocai-diagnosis.png differ diff --git a/docs/images/repository/repository-build105-overview.png b/docs/images/repository/repository-build105-overview.png new file mode 100644 index 0000000000000000000000000000000000000000..86f7c1befc321e51354b4226b3036603b7c9ad9b Binary files /dev/null and b/docs/images/repository/repository-build105-overview.png differ diff --git a/docs/images/repository/repository-build105-task-detail.png b/docs/images/repository/repository-build105-task-detail.png new file mode 100644 index 0000000000000000000000000000000000000000..00b2a992b3bad4972cecba655e971f631f791763 Binary files /dev/null and b/docs/images/repository/repository-build105-task-detail.png differ diff --git a/docs/images/repository/repository-build105-tasks.png b/docs/images/repository/repository-build105-tasks.png new file mode 100644 index 0000000000000000000000000000000000000000..49f7f2e89fa4cab13a71e391d583bb526af13c59 Binary files /dev/null and b/docs/images/repository/repository-build105-tasks.png differ diff --git a/docs/images/repository/repository-build105-upstream-source-form.png b/docs/images/repository/repository-build105-upstream-source-form.png new file mode 100644 index 0000000000000000000000000000000000000000..866261b7582f799b3df9a27adca89b56f3e787e0 Binary files /dev/null and b/docs/images/repository/repository-build105-upstream-source-form.png differ diff --git a/docs/images/repository/repository-build105-upstream-sources.png b/docs/images/repository/repository-build105-upstream-sources.png new file mode 100644 index 0000000000000000000000000000000000000000..f02a42abbbc9692947ca7131ccdfd6c238775240 Binary files /dev/null and b/docs/images/repository/repository-build105-upstream-sources.png differ diff --git a/docs/images/repository/repository-build141-create-filter-options.png b/docs/images/repository/repository-build141-create-filter-options.png new file mode 100644 index 0000000000000000000000000000000000000000..5a6c029aa1af132b6236cf7aeb25788c190229e9 Binary files /dev/null and b/docs/images/repository/repository-build141-create-filter-options.png differ diff --git a/docs/images/repository/repository-build141-create-form.png b/docs/images/repository/repository-build141-create-form.png new file mode 100644 index 0000000000000000000000000000000000000000..1125ec04e175714c5ce0961926e41c310c261155 Binary files /dev/null and b/docs/images/repository/repository-build141-create-form.png differ diff --git a/docs/images/repository/repository-build141-create-upstream-options.png b/docs/images/repository/repository-build141-create-upstream-options.png new file mode 100644 index 0000000000000000000000000000000000000000..ab2af310b2f090b1aa92641531a98a0420ac5155 Binary files /dev/null and b/docs/images/repository/repository-build141-create-upstream-options.png differ diff --git a/docs/images/repository/repository-build141-list.png b/docs/images/repository/repository-build141-list.png new file mode 100644 index 0000000000000000000000000000000000000000..c8b5be7383829ee8a3d7446d1c40d77ad589782e Binary files /dev/null and b/docs/images/repository/repository-build141-list.png differ diff --git a/docs/images/repository/repository-build141-upstream-sources.png b/docs/images/repository/repository-build141-upstream-sources.png new file mode 100644 index 0000000000000000000000000000000000000000..2072bf0bfe0a0d1c15416d5417322a52d3bd2c38 Binary files /dev/null and b/docs/images/repository/repository-build141-upstream-sources.png differ diff --git a/docs/images/repository/repository-current-20261024-arm-node-drawer.png b/docs/images/repository/repository-current-20261024-arm-node-drawer.png new file mode 100644 index 0000000000000000000000000000000000000000..a7e9a032dd7e69a5cc6a863fa1f9c8aa8482f6d5 Binary files /dev/null and b/docs/images/repository/repository-current-20261024-arm-node-drawer.png differ diff --git a/docs/images/repository/repository-current-20261024-batch-delete-preview.png b/docs/images/repository/repository-current-20261024-batch-delete-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..f8c310e0f8aeb7f4a1e2ee2edacd203ca291dc94 Binary files /dev/null and b/docs/images/repository/repository-current-20261024-batch-delete-preview.png differ diff --git a/docs/images/repository/repository-current-20261024-batch-delete-result.png b/docs/images/repository/repository-current-20261024-batch-delete-result.png new file mode 100644 index 0000000000000000000000000000000000000000..f00b54c8dbfd64397559f9c5d7e1a3e9a4bfab91 Binary files /dev/null and b/docs/images/repository/repository-current-20261024-batch-delete-result.png differ diff --git a/docs/images/repository/repository-current-20261024-batch20-distribution-detail.png b/docs/images/repository/repository-current-20261024-batch20-distribution-detail.png new file mode 100644 index 0000000000000000000000000000000000000000..010aaa015d8de7ec10087cb1a0a8c7012fec221a Binary files /dev/null and b/docs/images/repository/repository-current-20261024-batch20-distribution-detail.png differ diff --git a/docs/images/repository/repository-current-20261024-batch20-packages.png b/docs/images/repository/repository-current-20261024-batch20-packages.png new file mode 100644 index 0000000000000000000000000000000000000000..2b2305cae3eabe78668e92bd0bc54ed8f8277449 Binary files /dev/null and b/docs/images/repository/repository-current-20261024-batch20-packages.png differ diff --git a/docs/images/repository/repository-current-20261024-columns.png b/docs/images/repository/repository-current-20261024-columns.png new file mode 100644 index 0000000000000000000000000000000000000000..59a7b3b68836f6742f8781a356198e5131138ccb Binary files /dev/null and b/docs/images/repository/repository-current-20261024-columns.png differ diff --git a/docs/images/repository/repository-current-20261024-command-helper-preview.png b/docs/images/repository/repository-current-20261024-command-helper-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..b1f7e1d4c8927d0b7f297ea328e2201006b2c5f4 Binary files /dev/null and b/docs/images/repository/repository-current-20261024-command-helper-preview.png differ diff --git a/docs/images/repository/repository-current-20261024-command-helper-rerun.png b/docs/images/repository/repository-current-20261024-command-helper-rerun.png new file mode 100644 index 0000000000000000000000000000000000000000..e40de85f8d7078d29f2f9002915e0f79de47402a Binary files /dev/null and b/docs/images/repository/repository-current-20261024-command-helper-rerun.png differ diff --git a/docs/images/repository/repository-current-20261024-config.png b/docs/images/repository/repository-current-20261024-config.png new file mode 100644 index 0000000000000000000000000000000000000000..2856f946eddb5688d88df5fcd4b80585a706a7b6 Binary files /dev/null and b/docs/images/repository/repository-current-20261024-config.png differ diff --git a/docs/images/repository/repository-current-20261024-create-filter.png b/docs/images/repository/repository-current-20261024-create-filter.png new file mode 100644 index 0000000000000000000000000000000000000000..64fb0390619d2a7c0350cf0104ae59c4e12a42e4 Binary files /dev/null and b/docs/images/repository/repository-current-20261024-create-filter.png differ diff --git a/docs/images/repository/repository-current-20261024-create-noarch-filter.png b/docs/images/repository/repository-current-20261024-create-noarch-filter.png new file mode 100644 index 0000000000000000000000000000000000000000..07bf50d26fec105b3c073183386c6fa4a32f51a2 Binary files /dev/null and b/docs/images/repository/repository-current-20261024-create-noarch-filter.png differ diff --git a/docs/images/repository/repository-current-20261024-create-upstream.png b/docs/images/repository/repository-current-20261024-create-upstream.png new file mode 100644 index 0000000000000000000000000000000000000000..77999317f16d4b620b4135e5b300d7642c1ac515 Binary files /dev/null and b/docs/images/repository/repository-current-20261024-create-upstream.png differ diff --git a/docs/images/repository/repository-current-20261024-disk-chart.png b/docs/images/repository/repository-current-20261024-disk-chart.png new file mode 100644 index 0000000000000000000000000000000000000000..780267df3a5cd656915a94f746d8f9e7762ffc20 Binary files /dev/null and b/docs/images/repository/repository-current-20261024-disk-chart.png differ diff --git a/docs/images/repository/repository-current-20261024-distribution-detail.png b/docs/images/repository/repository-current-20261024-distribution-detail.png new file mode 100644 index 0000000000000000000000000000000000000000..010aaa015d8de7ec10087cb1a0a8c7012fec221a Binary files /dev/null and b/docs/images/repository/repository-current-20261024-distribution-detail.png differ diff --git a/docs/images/repository/repository-current-20261024-distribution.png b/docs/images/repository/repository-current-20261024-distribution.png new file mode 100644 index 0000000000000000000000000000000000000000..c1682cd832c55531a562a17540e49a8c082fa845 Binary files /dev/null and b/docs/images/repository/repository-current-20261024-distribution.png differ diff --git a/docs/images/repository/repository-current-20261024-gpg.png b/docs/images/repository/repository-current-20261024-gpg.png new file mode 100644 index 0000000000000000000000000000000000000000..c900e26304cda542e14ff3572b4b0ca3f6b1bfcf Binary files /dev/null and b/docs/images/repository/repository-current-20261024-gpg.png differ diff --git a/docs/images/repository/repository-current-20261024-inventory-import-preview.png b/docs/images/repository/repository-current-20261024-inventory-import-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..51bdd34816f479f0f87f42547fa40c1558c135a6 Binary files /dev/null and b/docs/images/repository/repository-current-20261024-inventory-import-preview.png differ diff --git a/docs/images/repository/repository-current-20261024-inventory-import-task.png b/docs/images/repository/repository-current-20261024-inventory-import-task.png new file mode 100644 index 0000000000000000000000000000000000000000..6111d4369a900e75dadb7743d28cc9cb94841a97 Binary files /dev/null and b/docs/images/repository/repository-current-20261024-inventory-import-task.png differ diff --git a/docs/images/repository/repository-current-20261024-list.png b/docs/images/repository/repository-current-20261024-list.png new file mode 100644 index 0000000000000000000000000000000000000000..4cce28027507135dd4f2f62b6395fc9fc034798b Binary files /dev/null and b/docs/images/repository/repository-current-20261024-list.png differ diff --git a/docs/images/repository/repository-current-20261024-node-allowlist.png b/docs/images/repository/repository-current-20261024-node-allowlist.png new file mode 100644 index 0000000000000000000000000000000000000000..cbc40135b0e60a53dcc70ae1b816c5b806b77ff2 Binary files /dev/null and b/docs/images/repository/repository-current-20261024-node-allowlist.png differ diff --git a/docs/images/repository/repository-current-20261024-node-drawer.png b/docs/images/repository/repository-current-20261024-node-drawer.png new file mode 100644 index 0000000000000000000000000000000000000000..10bda26e5e405a0fa932c024634080c388dddfbc Binary files /dev/null and b/docs/images/repository/repository-current-20261024-node-drawer.png differ diff --git a/docs/images/repository/repository-current-20261024-node-install-sources.png b/docs/images/repository/repository-current-20261024-node-install-sources.png new file mode 100644 index 0000000000000000000000000000000000000000..ffeaebafe03d856249dc3abfd5cff45e5556703e Binary files /dev/null and b/docs/images/repository/repository-current-20261024-node-install-sources.png differ diff --git a/docs/images/repository/repository-current-20261024-node-install.png b/docs/images/repository/repository-current-20261024-node-install.png new file mode 100644 index 0000000000000000000000000000000000000000..3c24e64baabf179fa27c43f72f937983fd4ccc31 Binary files /dev/null and b/docs/images/repository/repository-current-20261024-node-install.png differ diff --git a/docs/images/repository/repository-current-20261024-nodes.png b/docs/images/repository/repository-current-20261024-nodes.png new file mode 100644 index 0000000000000000000000000000000000000000..9c29db7ac219f1f99d44e8a8798eb12efaddfd4a Binary files /dev/null and b/docs/images/repository/repository-current-20261024-nodes.png differ diff --git a/docs/images/repository/repository-current-20261024-ocai-context.png b/docs/images/repository/repository-current-20261024-ocai-context.png new file mode 100644 index 0000000000000000000000000000000000000000..2fdd35976adbc599892016ddfa483794963102ba Binary files /dev/null and b/docs/images/repository/repository-current-20261024-ocai-context.png differ diff --git a/docs/images/repository/repository-current-20261024-ocai-result.png b/docs/images/repository/repository-current-20261024-ocai-result.png new file mode 100644 index 0000000000000000000000000000000000000000..efe117edd233b307cdd622b88516c79f56f80444 Binary files /dev/null and b/docs/images/repository/repository-current-20261024-ocai-result.png differ diff --git a/docs/images/repository/repository-current-20261024-overview.png b/docs/images/repository/repository-current-20261024-overview.png new file mode 100644 index 0000000000000000000000000000000000000000..f280371c9640f2aad3a2ea70153920d99068b7dc Binary files /dev/null and b/docs/images/repository/repository-current-20261024-overview.png differ diff --git a/docs/images/repository/repository-current-20261024-package-install-nodes.png b/docs/images/repository/repository-current-20261024-package-install-nodes.png new file mode 100644 index 0000000000000000000000000000000000000000..e5435a7b65e3ac5cec5b5d37bc8df38554c32d64 Binary files /dev/null and b/docs/images/repository/repository-current-20261024-package-install-nodes.png differ diff --git a/docs/images/repository/repository-current-20261024-package-install-preview.png b/docs/images/repository/repository-current-20261024-package-install-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..e79f6070a6a04dd49b06b15a0bc7ed5309baafa1 Binary files /dev/null and b/docs/images/repository/repository-current-20261024-package-install-preview.png differ diff --git a/docs/images/repository/repository-current-20261024-package-install-result.png b/docs/images/repository/repository-current-20261024-package-install-result.png new file mode 100644 index 0000000000000000000000000000000000000000..44fca72e7d92d8f13bc0b9017631230ea684bf7f Binary files /dev/null and b/docs/images/repository/repository-current-20261024-package-install-result.png differ diff --git a/docs/images/repository/repository-current-20261024-packages.png b/docs/images/repository/repository-current-20261024-packages.png new file mode 100644 index 0000000000000000000000000000000000000000..894d96f26c5c42ef897ac5fc0f3e174311f05108 Binary files /dev/null and b/docs/images/repository/repository-current-20261024-packages.png differ diff --git a/docs/images/repository/repository-current-20261024-quarantine.png b/docs/images/repository/repository-current-20261024-quarantine.png new file mode 100644 index 0000000000000000000000000000000000000000..b0fc8abe4d1e7146bc9264f24a0b090015ddcd7d Binary files /dev/null and b/docs/images/repository/repository-current-20261024-quarantine.png differ diff --git a/docs/images/repository/repository-current-20261024-source-form.png b/docs/images/repository/repository-current-20261024-source-form.png new file mode 100644 index 0000000000000000000000000000000000000000..22eefdaa730682e35df30fcfeeef40208863a150 Binary files /dev/null and b/docs/images/repository/repository-current-20261024-source-form.png differ diff --git a/docs/images/repository/repository-current-20261024-sources.png b/docs/images/repository/repository-current-20261024-sources.png new file mode 100644 index 0000000000000000000000000000000000000000..6eef9babd5e3217b62c0c2ec2367ab3898f2b2d9 Binary files /dev/null and b/docs/images/repository/repository-current-20261024-sources.png differ diff --git a/docs/images/repository/repository-current-20261024-task-failure.png b/docs/images/repository/repository-current-20261024-task-failure.png new file mode 100644 index 0000000000000000000000000000000000000000..9209369c6694456bd4d78169cdb087359c99f472 Binary files /dev/null and b/docs/images/repository/repository-current-20261024-task-failure.png differ diff --git a/docs/images/repository/repository-current-20261024-task-success.png b/docs/images/repository/repository-current-20261024-task-success.png new file mode 100644 index 0000000000000000000000000000000000000000..44fca72e7d92d8f13bc0b9017631230ea684bf7f Binary files /dev/null and b/docs/images/repository/repository-current-20261024-task-success.png differ diff --git a/docs/images/repository/repository-current-20261024-tasks.png b/docs/images/repository/repository-current-20261024-tasks.png new file mode 100644 index 0000000000000000000000000000000000000000..c9ff555963c93716ed7e9d76cb5e64e898d53cce Binary files /dev/null and b/docs/images/repository/repository-current-20261024-tasks.png differ diff --git a/docs/images/repository/repository-current-20261024-upload-queue.png b/docs/images/repository/repository-current-20261024-upload-queue.png new file mode 100644 index 0000000000000000000000000000000000000000..afbd70667491b02ea3e3a2b6975c54e39958ac04 Binary files /dev/null and b/docs/images/repository/repository-current-20261024-upload-queue.png differ diff --git a/docs/images/repository/repository-current-add-node.png b/docs/images/repository/repository-current-add-node.png new file mode 100644 index 0000000000000000000000000000000000000000..c1b4632ed69098d5ee28fb5e190e2e24a2a66933 Binary files /dev/null and b/docs/images/repository/repository-current-add-node.png differ diff --git a/docs/images/repository/repository-current-create-source-filters.png b/docs/images/repository/repository-current-create-source-filters.png new file mode 100644 index 0000000000000000000000000000000000000000..4a2aef976f7dde986ab9c3feb2aa98acfbd961bd Binary files /dev/null and b/docs/images/repository/repository-current-create-source-filters.png differ diff --git a/docs/images/repository/repository-current-create-source-profile-20260909.png b/docs/images/repository/repository-current-create-source-profile-20260909.png new file mode 100644 index 0000000000000000000000000000000000000000..0ea412cc6c5a19228b3575bd2289b5d90f8dfb32 Binary files /dev/null and b/docs/images/repository/repository-current-create-source-profile-20260909.png differ diff --git a/docs/images/repository/repository-current-create-source-profile.png b/docs/images/repository/repository-current-create-source-profile.png new file mode 100644 index 0000000000000000000000000000000000000000..7b4af2135a05f2c0d7f51014a218441563df6335 Binary files /dev/null and b/docs/images/repository/repository-current-create-source-profile.png differ diff --git a/docs/images/repository/repository-current-create.png b/docs/images/repository/repository-current-create.png new file mode 100644 index 0000000000000000000000000000000000000000..e79f5749e989902bd40605414f45e534582620fb Binary files /dev/null and b/docs/images/repository/repository-current-create.png differ diff --git a/docs/images/repository/repository-current-delete-node.png b/docs/images/repository/repository-current-delete-node.png new file mode 100644 index 0000000000000000000000000000000000000000..d68cd55c2478cc121c317d21433c41c68cc78a47 Binary files /dev/null and b/docs/images/repository/repository-current-delete-node.png differ diff --git a/docs/images/repository/repository-current-delete-package.png b/docs/images/repository/repository-current-delete-package.png new file mode 100644 index 0000000000000000000000000000000000000000..b926b0340ef7c0ca44fd7b2909368306d81df1bf Binary files /dev/null and b/docs/images/repository/repository-current-delete-package.png differ diff --git a/docs/images/repository/repository-current-delete-repository.png b/docs/images/repository/repository-current-delete-repository.png new file mode 100644 index 0000000000000000000000000000000000000000..8bc2f27fff84ffa89c9e4330e680af89b17c59d5 Binary files /dev/null and b/docs/images/repository/repository-current-delete-repository.png differ diff --git a/docs/images/repository/repository-current-distribution-create.png b/docs/images/repository/repository-current-distribution-create.png new file mode 100644 index 0000000000000000000000000000000000000000..26d654d875120fc177957a28f3d9e90d2833ce98 Binary files /dev/null and b/docs/images/repository/repository-current-distribution-create.png differ diff --git a/docs/images/repository/repository-current-edit-repository.png b/docs/images/repository/repository-current-edit-repository.png new file mode 100644 index 0000000000000000000000000000000000000000..335064ea6fe402f790b18e7f877a2ce822bd3424 Binary files /dev/null and b/docs/images/repository/repository-current-edit-repository.png differ diff --git a/docs/images/repository/repository-current-gpg-keys.png b/docs/images/repository/repository-current-gpg-keys.png new file mode 100644 index 0000000000000000000000000000000000000000..6f7af2685ef93cacf133a12d0be26781e82db5c2 Binary files /dev/null and b/docs/images/repository/repository-current-gpg-keys.png differ diff --git a/docs/images/repository/repository-current-inventory-download.png b/docs/images/repository/repository-current-inventory-download.png new file mode 100644 index 0000000000000000000000000000000000000000..af5ab06cb369a171c65736871e7af8ae38be6350 Binary files /dev/null and b/docs/images/repository/repository-current-inventory-download.png differ diff --git a/docs/images/repository/repository-current-inventory-export-action.png b/docs/images/repository/repository-current-inventory-export-action.png new file mode 100644 index 0000000000000000000000000000000000000000..5ffb5e6d4dbadff556fd58d6c01cd9bfbb14dc40 Binary files /dev/null and b/docs/images/repository/repository-current-inventory-export-action.png differ diff --git a/docs/images/repository/repository-current-list-node-entry.png b/docs/images/repository/repository-current-list-node-entry.png new file mode 100644 index 0000000000000000000000000000000000000000..634e3b95ca37e6e9ceb872f4f63bb9bf5b2c60ad Binary files /dev/null and b/docs/images/repository/repository-current-list-node-entry.png differ diff --git a/docs/images/repository/repository-current-list.png b/docs/images/repository/repository-current-list.png new file mode 100644 index 0000000000000000000000000000000000000000..f034eaa09195ef63c22935690f976aeec846a81b Binary files /dev/null and b/docs/images/repository/repository-current-list.png differ diff --git a/docs/images/repository/repository-current-node-capacity-action.png b/docs/images/repository/repository-current-node-capacity-action.png new file mode 100644 index 0000000000000000000000000000000000000000..83fb8ea132a20bf329c17bb90aa20237482f9473 Binary files /dev/null and b/docs/images/repository/repository-current-node-capacity-action.png differ diff --git a/docs/images/repository/repository-current-node-capacity-drawer.png b/docs/images/repository/repository-current-node-capacity-drawer.png new file mode 100644 index 0000000000000000000000000000000000000000..f7c7350022aa8afac7a0cba9a0bfdedcc239e541 Binary files /dev/null and b/docs/images/repository/repository-current-node-capacity-drawer.png differ diff --git a/docs/images/repository/repository-current-node-edit-drawer.png b/docs/images/repository/repository-current-node-edit-drawer.png new file mode 100644 index 0000000000000000000000000000000000000000..b1b01ce0f644a9e8cb15fe84c3bf75ea5196bfba Binary files /dev/null and b/docs/images/repository/repository-current-node-edit-drawer.png differ diff --git a/docs/images/repository/repository-current-node-list.png b/docs/images/repository/repository-current-node-list.png new file mode 100644 index 0000000000000000000000000000000000000000..1b62185c1c4ac44c631846f2e14ce5e009adacb2 Binary files /dev/null and b/docs/images/repository/repository-current-node-list.png differ diff --git a/docs/images/repository/repository-current-node-probe-action.png b/docs/images/repository/repository-current-node-probe-action.png new file mode 100644 index 0000000000000000000000000000000000000000..9c261a0792bc996032339ed6336c16e6ad02be80 Binary files /dev/null and b/docs/images/repository/repository-current-node-probe-action.png differ diff --git a/docs/images/repository/repository-current-ocai-context.png b/docs/images/repository/repository-current-ocai-context.png new file mode 100644 index 0000000000000000000000000000000000000000..da5ad96bf0565de37fb6197fa677eb222e962488 Binary files /dev/null and b/docs/images/repository/repository-current-ocai-context.png differ diff --git a/docs/images/repository/repository-current-overview-private.png b/docs/images/repository/repository-current-overview-private.png new file mode 100644 index 0000000000000000000000000000000000000000..16d1a960e3e47c8e20fbbcd1639a1d435cbd0704 Binary files /dev/null and b/docs/images/repository/repository-current-overview-private.png differ diff --git a/docs/images/repository/repository-current-overview-sync.png b/docs/images/repository/repository-current-overview-sync.png new file mode 100644 index 0000000000000000000000000000000000000000..3202b38622498175bd0fea281f1c1e871250bdd2 Binary files /dev/null and b/docs/images/repository/repository-current-overview-sync.png differ diff --git a/docs/images/repository/repository-current-packages-private.png b/docs/images/repository/repository-current-packages-private.png new file mode 100644 index 0000000000000000000000000000000000000000..b39137c4de16aeca9fd0423ce09628cdea726d51 Binary files /dev/null and b/docs/images/repository/repository-current-packages-private.png differ diff --git a/docs/images/repository/repository-current-quarantine-rejected.png b/docs/images/repository/repository-current-quarantine-rejected.png new file mode 100644 index 0000000000000000000000000000000000000000..e91d580ad0e64cfa5f3a37ad025e8e0859db004d Binary files /dev/null and b/docs/images/repository/repository-current-quarantine-rejected.png differ diff --git a/docs/images/repository/repository-current-repo-config.png b/docs/images/repository/repository-current-repo-config.png new file mode 100644 index 0000000000000000000000000000000000000000..e6386d65731b8331694cd85965ecc0400f81dee6 Binary files /dev/null and b/docs/images/repository/repository-current-repo-config.png differ diff --git a/docs/images/repository/repository-current-row-actions.png b/docs/images/repository/repository-current-row-actions.png new file mode 100644 index 0000000000000000000000000000000000000000..69d3d00b9aea95a2d324329be8eeb4c70c91aa23 Binary files /dev/null and b/docs/images/repository/repository-current-row-actions.png differ diff --git a/docs/images/repository/repository-current-storage-nodes.png b/docs/images/repository/repository-current-storage-nodes.png new file mode 100644 index 0000000000000000000000000000000000000000..8af6747ab939c15c3ea2b5e05653c2f3b368be54 Binary files /dev/null and b/docs/images/repository/repository-current-storage-nodes.png differ diff --git a/docs/images/repository/repository-current-synced-packages.png b/docs/images/repository/repository-current-synced-packages.png new file mode 100644 index 0000000000000000000000000000000000000000..24e2727a841caf232f3d3d95d4e28c2a8b060ae6 Binary files /dev/null and b/docs/images/repository/repository-current-synced-packages.png differ diff --git a/docs/images/repository/repository-current-task-detail.png b/docs/images/repository/repository-current-task-detail.png new file mode 100644 index 0000000000000000000000000000000000000000..a3ae45cf28b450514a9e62f42fa7eb10f2fb8ec2 Binary files /dev/null and b/docs/images/repository/repository-current-task-detail.png differ diff --git a/docs/images/repository/repository-current-task-diagnosis.png b/docs/images/repository/repository-current-task-diagnosis.png new file mode 100644 index 0000000000000000000000000000000000000000..037d8ec205d542a2f21ac0b1ca0e359ae5a7f13f Binary files /dev/null and b/docs/images/repository/repository-current-task-diagnosis.png differ diff --git a/docs/images/repository/repository-current-tasks.png b/docs/images/repository/repository-current-tasks.png new file mode 100644 index 0000000000000000000000000000000000000000..99a6ab8e0232945c8c70ffcbb83fe180dab138b0 Binary files /dev/null and b/docs/images/repository/repository-current-tasks.png differ diff --git a/docs/images/repository/repository-current-upload-selected.png b/docs/images/repository/repository-current-upload-selected.png new file mode 100644 index 0000000000000000000000000000000000000000..ce1a393b0983674a9b4300659e87ca89ad9f9597 Binary files /dev/null and b/docs/images/repository/repository-current-upload-selected.png differ diff --git a/docs/images/repository/repository-current-upstream-source-form.png b/docs/images/repository/repository-current-upstream-source-form.png new file mode 100644 index 0000000000000000000000000000000000000000..9b8b34d15e28d1986730334025556813e467625b Binary files /dev/null and b/docs/images/repository/repository-current-upstream-source-form.png differ diff --git a/docs/images/repository/repository-current-upstream-sources.png b/docs/images/repository/repository-current-upstream-sources.png new file mode 100644 index 0000000000000000000000000000000000000000..65e250b4dcd20548985992a8c47b8f9341285117 Binary files /dev/null and b/docs/images/repository/repository-current-upstream-sources.png differ diff --git a/docs/images/repository/repository-final-20261024-node-version.png b/docs/images/repository/repository-final-20261024-node-version.png new file mode 100644 index 0000000000000000000000000000000000000000..64d4c33632f96371fed03ced4ea606c8918364b8 Binary files /dev/null and b/docs/images/repository/repository-final-20261024-node-version.png differ diff --git a/docs/images/repository/repository-final-20261024-tasks.png b/docs/images/repository/repository-final-20261024-tasks.png new file mode 100644 index 0000000000000000000000000000000000000000..dcbe2df27521126b405593c43d0badedb21c78ef Binary files /dev/null and b/docs/images/repository/repository-final-20261024-tasks.png differ diff --git a/docs/images/repository/repository-final-20261024-upload-complete.png b/docs/images/repository/repository-final-20261024-upload-complete.png new file mode 100644 index 0000000000000000000000000000000000000000..5d8467c6a340679c596159245d13392252069036 Binary files /dev/null and b/docs/images/repository/repository-final-20261024-upload-complete.png differ diff --git a/docs/images/repository/repository-guide-01-entry-list.png b/docs/images/repository/repository-guide-01-entry-list.png new file mode 100644 index 0000000000000000000000000000000000000000..a545506dcdd5d415eb5a0e6b5f39c637e689b5f9 Binary files /dev/null and b/docs/images/repository/repository-guide-01-entry-list.png differ diff --git a/docs/images/repository/repository-guide-02-node-list.png b/docs/images/repository/repository-guide-02-node-list.png new file mode 100644 index 0000000000000000000000000000000000000000..eacab89e20aad464959eef38a406f40be4f286f4 Binary files /dev/null and b/docs/images/repository/repository-guide-02-node-list.png differ diff --git a/docs/images/repository/repository-guide-02-register-node-form.png b/docs/images/repository/repository-guide-02-register-node-form.png new file mode 100644 index 0000000000000000000000000000000000000000..0885e12b98d86d15469cf96f3c7494ef579eded4 Binary files /dev/null and b/docs/images/repository/repository-guide-02-register-node-form.png differ diff --git a/docs/images/repository/repository-guide-03-node-preflight.png b/docs/images/repository/repository-guide-03-node-preflight.png new file mode 100644 index 0000000000000000000000000000000000000000..fc79183317b4c04b5be67ef0a6014d1b775eeae5 Binary files /dev/null and b/docs/images/repository/repository-guide-03-node-preflight.png differ diff --git a/docs/images/repository/repository-guide-04-create-repository-form.png b/docs/images/repository/repository-guide-04-create-repository-form.png new file mode 100644 index 0000000000000000000000000000000000000000..9d15ac8754fdebc4da77f3e04005f2a857a16f41 Binary files /dev/null and b/docs/images/repository/repository-guide-04-create-repository-form.png differ diff --git a/docs/images/repository/repository-guide-04-noarch-filter.png b/docs/images/repository/repository-guide-04-noarch-filter.png new file mode 100644 index 0000000000000000000000000000000000000000..c15a335ea9a1a3f002dfbb1c1dda78f692de4f56 Binary files /dev/null and b/docs/images/repository/repository-guide-04-noarch-filter.png differ diff --git a/docs/images/repository/repository-guide-04-repository-overview.png b/docs/images/repository/repository-guide-04-repository-overview.png new file mode 100644 index 0000000000000000000000000000000000000000..52873b6510aec0c18be245577a1b47bac70989f6 Binary files /dev/null and b/docs/images/repository/repository-guide-04-repository-overview.png differ diff --git a/docs/images/repository/repository-guide-05-sync-overview.png b/docs/images/repository/repository-guide-05-sync-overview.png new file mode 100644 index 0000000000000000000000000000000000000000..b8ab9f9d7fec8e97e6a48a3aa146fa28a024b4ea Binary files /dev/null and b/docs/images/repository/repository-guide-05-sync-overview.png differ diff --git a/docs/images/repository/repository-guide-05-sync-packages.png b/docs/images/repository/repository-guide-05-sync-packages.png new file mode 100644 index 0000000000000000000000000000000000000000..59add557b828ab76e111c8ff2fed442380033a94 Binary files /dev/null and b/docs/images/repository/repository-guide-05-sync-packages.png differ diff --git a/docs/images/repository/repository-guide-06-private-upload-selected.png b/docs/images/repository/repository-guide-06-private-upload-selected.png new file mode 100644 index 0000000000000000000000000000000000000000..7dab32e2e63b0cb9e2f62ec1eb7afe989c31e89e Binary files /dev/null and b/docs/images/repository/repository-guide-06-private-upload-selected.png differ diff --git a/docs/images/repository/repository-guide-07a-packages.png b/docs/images/repository/repository-guide-07a-packages.png new file mode 100644 index 0000000000000000000000000000000000000000..c26a393e18f6ea469fa02e98c9912004b4199963 Binary files /dev/null and b/docs/images/repository/repository-guide-07a-packages.png differ diff --git a/docs/images/repository/repository-guide-07b-quarantine.png b/docs/images/repository/repository-guide-07b-quarantine.png new file mode 100644 index 0000000000000000000000000000000000000000..811d67ffbc0d27718570b4b4d3f9d7c741a73aa5 Binary files /dev/null and b/docs/images/repository/repository-guide-07b-quarantine.png differ diff --git a/docs/images/repository/repository-guide-07c-task-detail.png b/docs/images/repository/repository-guide-07c-task-detail.png new file mode 100644 index 0000000000000000000000000000000000000000..0874e7cdceba5cc2fe2170e408832f20bbcce584 Binary files /dev/null and b/docs/images/repository/repository-guide-07c-task-detail.png differ diff --git a/docs/images/repository/repository-guide-07c-tasks.png b/docs/images/repository/repository-guide-07c-tasks.png new file mode 100644 index 0000000000000000000000000000000000000000..7b35a76d85d52b7d0f796fd127a6309fe342f2f4 Binary files /dev/null and b/docs/images/repository/repository-guide-07c-tasks.png differ diff --git a/docs/images/repository/repository-guide-07d-ocai-diagnosis.png b/docs/images/repository/repository-guide-07d-ocai-diagnosis.png new file mode 100644 index 0000000000000000000000000000000000000000..4cd989d2fc0d7ed3dcd59a3cc4a5b85989ee8c99 Binary files /dev/null and b/docs/images/repository/repository-guide-07d-ocai-diagnosis.png differ diff --git a/docs/images/repository/repository-guide-08-repo-config.png b/docs/images/repository/repository-guide-08-repo-config.png new file mode 100644 index 0000000000000000000000000000000000000000..3fa190833cd2196d3e31971280415a45e2205f27 Binary files /dev/null and b/docs/images/repository/repository-guide-08-repo-config.png differ diff --git a/docs/images/repository/repository-guide-09-distribution-result.png b/docs/images/repository/repository-guide-09-distribution-result.png new file mode 100644 index 0000000000000000000000000000000000000000..39863ed766702cf2d2b84c346667396a7fd0024c Binary files /dev/null and b/docs/images/repository/repository-guide-09-distribution-result.png differ diff --git a/docs/images/repository/repository-guide-09-distribution-select.png b/docs/images/repository/repository-guide-09-distribution-select.png new file mode 100644 index 0000000000000000000000000000000000000000..472f6a0fdc031d3d8dcd84d041c18a47b4ade246 Binary files /dev/null and b/docs/images/repository/repository-guide-09-distribution-select.png differ diff --git a/docs/images/repository/repository-p13-arm-node-repository.png b/docs/images/repository/repository-p13-arm-node-repository.png new file mode 100644 index 0000000000000000000000000000000000000000..23650d7c777f1e451e2153c36ea643fda6015b3b Binary files /dev/null and b/docs/images/repository/repository-p13-arm-node-repository.png differ diff --git a/docs/images/repository/repository-p13-batch-delete-preview.png b/docs/images/repository/repository-p13-batch-delete-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..2320c392c6209efc49a11e8113f35845c9fed35e Binary files /dev/null and b/docs/images/repository/repository-p13-batch-delete-preview.png differ diff --git a/docs/images/repository/repository-p13-batch-delete-result.png b/docs/images/repository/repository-p13-batch-delete-result.png new file mode 100644 index 0000000000000000000000000000000000000000..f78c04481734dff36df18cb83bbe79dcc2f63271 Binary files /dev/null and b/docs/images/repository/repository-p13-batch-delete-result.png differ diff --git a/docs/images/repository/repository-p13-inventory-import-preview.png b/docs/images/repository/repository-p13-inventory-import-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..4fa43d6ba8db83c8ed9b26ba4f278252b1797a4a Binary files /dev/null and b/docs/images/repository/repository-p13-inventory-import-preview.png differ diff --git a/docs/images/repository/repository-p13-inventory-import-task.png b/docs/images/repository/repository-p13-inventory-import-task.png new file mode 100644 index 0000000000000000000000000000000000000000..ad1211db8b77e868f56574e40609f124ee9e95b8 Binary files /dev/null and b/docs/images/repository/repository-p13-inventory-import-task.png differ diff --git a/docs/images/repository/repository-p13-node-view-sync.png b/docs/images/repository/repository-p13-node-view-sync.png new file mode 100644 index 0000000000000000000000000000000000000000..9f9278128810821c11891c4cb57a1f5a631afe07 Binary files /dev/null and b/docs/images/repository/repository-p13-node-view-sync.png differ diff --git a/docs/images/repository/repository-p13-package-install-preview.png b/docs/images/repository/repository-p13-package-install-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..c2bceaadc785aeb8136b1208861637f67afe8d3d Binary files /dev/null and b/docs/images/repository/repository-p13-package-install-preview.png differ diff --git a/docs/images/repository/repository-p13-package-install-targets.png b/docs/images/repository/repository-p13-package-install-targets.png new file mode 100644 index 0000000000000000000000000000000000000000..260e5b08b240a487014613d933f2cde6d65c6482 Binary files /dev/null and b/docs/images/repository/repository-p13-package-install-targets.png differ diff --git a/docs/images/repository/repository-p13-x86-node-repository.png b/docs/images/repository/repository-p13-x86-node-repository.png new file mode 100644 index 0000000000000000000000000000000000000000..e0d009f3c8e747e2adb13548440f6be34d27b8ca Binary files /dev/null and b/docs/images/repository/repository-p13-x86-node-repository.png differ diff --git a/docs/images/repository/repository-p5-package-install-preview.png b/docs/images/repository/repository-p5-package-install-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..d77b05b2fa8e6cf1b9a2da55515cc141bf996c57 Binary files /dev/null and b/docs/images/repository/repository-p5-package-install-preview.png differ diff --git a/docs/images/repository/repository-p5-task-target-results.png b/docs/images/repository/repository-p5-task-target-results.png new file mode 100644 index 0000000000000000000000000000000000000000..0ef74c450bf0a51e9a4b15cfd259f00873937369 Binary files /dev/null and b/docs/images/repository/repository-p5-task-target-results.png differ diff --git a/docs/images/repository/repository-p6-package-delete-preview.png b/docs/images/repository/repository-p6-package-delete-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..1393da584d9e8a3c1992b770b3b076ba4e7f557f Binary files /dev/null and b/docs/images/repository/repository-p6-package-delete-preview.png differ diff --git a/docs/images/repository/repository-p6-resumable-upload-paused.png b/docs/images/repository/repository-p6-resumable-upload-paused.png new file mode 100644 index 0000000000000000000000000000000000000000..abb0f6be2b5df08d748d93406d789772f349634b Binary files /dev/null and b/docs/images/repository/repository-p6-resumable-upload-paused.png differ diff --git a/docs/images/repository/repository-p7-inventory-import.png b/docs/images/repository/repository-p7-inventory-import.png new file mode 100644 index 0000000000000000000000000000000000000000..6a04952005f19a8bb400b9d1a37f4b11c886d24e Binary files /dev/null and b/docs/images/repository/repository-p7-inventory-import.png differ diff --git a/docs/images/repository/repository-p7-list.png b/docs/images/repository/repository-p7-list.png new file mode 100644 index 0000000000000000000000000000000000000000..6bac6e37ad711287fccc6e7789b4de57ffdc1968 Binary files /dev/null and b/docs/images/repository/repository-p7-list.png differ diff --git a/docs/images/repository/repository-p7-metadata-key-inventory.png b/docs/images/repository/repository-p7-metadata-key-inventory.png new file mode 100644 index 0000000000000000000000000000000000000000..b16b92cbf2b74c453f26a5e4a17e3e033f68dc58 Binary files /dev/null and b/docs/images/repository/repository-p7-metadata-key-inventory.png differ diff --git a/docs/images/repository/repository-p8-dual-architecture-nodes.png b/docs/images/repository/repository-p8-dual-architecture-nodes.png new file mode 100644 index 0000000000000000000000000000000000000000..6f4ad1371e5526bd5755b371892e965504b15893 Binary files /dev/null and b/docs/images/repository/repository-p8-dual-architecture-nodes.png differ diff --git a/docs/images/repository/repository-release-config.png b/docs/images/repository/repository-release-config.png new file mode 100644 index 0000000000000000000000000000000000000000..d1e947ddb2bb0fbcda6385596dfea9d6a7092417 Binary files /dev/null and b/docs/images/repository/repository-release-config.png differ diff --git a/docs/images/repository/repository-release-create-filter.png b/docs/images/repository/repository-release-create-filter.png new file mode 100644 index 0000000000000000000000000000000000000000..33757695836e0e0e9a9edbc2d7cb9d69b69efd0f Binary files /dev/null and b/docs/images/repository/repository-release-create-filter.png differ diff --git a/docs/images/repository/repository-release-create-presets.png b/docs/images/repository/repository-release-create-presets.png new file mode 100644 index 0000000000000000000000000000000000000000..0f05f92531ad5d9921260d29164acd047a10d92b Binary files /dev/null and b/docs/images/repository/repository-release-create-presets.png differ diff --git a/docs/images/repository/repository-release-create.png b/docs/images/repository/repository-release-create.png new file mode 100644 index 0000000000000000000000000000000000000000..038fa324139e47540c15db87cedbae59d16f16b2 Binary files /dev/null and b/docs/images/repository/repository-release-create.png differ diff --git a/docs/images/repository/repository-release-distribution.png b/docs/images/repository/repository-release-distribution.png new file mode 100644 index 0000000000000000000000000000000000000000..82647fa4421b45cef2afeed7ed761c3d9da62d6f Binary files /dev/null and b/docs/images/repository/repository-release-distribution.png differ diff --git a/docs/images/repository/repository-release-gpg.png b/docs/images/repository/repository-release-gpg.png new file mode 100644 index 0000000000000000000000000000000000000000..e23ada8f3511410654aff3cf981426c1a2d6dda8 Binary files /dev/null and b/docs/images/repository/repository-release-gpg.png differ diff --git a/docs/images/repository/repository-release-list.png b/docs/images/repository/repository-release-list.png new file mode 100644 index 0000000000000000000000000000000000000000..ff750729465031a8e74650f5c3a712010bde60dd Binary files /dev/null and b/docs/images/repository/repository-release-list.png differ diff --git a/docs/images/repository/repository-release-node-allowlist.png b/docs/images/repository/repository-release-node-allowlist.png new file mode 100644 index 0000000000000000000000000000000000000000..02d1315d215c35cb5c80a026e3f8ffb253921d74 Binary files /dev/null and b/docs/images/repository/repository-release-node-allowlist.png differ diff --git a/docs/images/repository/repository-release-node-drawer-1440.png b/docs/images/repository/repository-release-node-drawer-1440.png new file mode 100644 index 0000000000000000000000000000000000000000..84e02bd5b8a2cf572bac72f59dfc3c1a4995bbc4 Binary files /dev/null and b/docs/images/repository/repository-release-node-drawer-1440.png differ diff --git a/docs/images/repository/repository-release-node-drawer.png b/docs/images/repository/repository-release-node-drawer.png new file mode 100644 index 0000000000000000000000000000000000000000..c41de13ecf7f79eccecd5715909f5ebae5f34413 Binary files /dev/null and b/docs/images/repository/repository-release-node-drawer.png differ diff --git a/docs/images/repository/repository-release-node-install.png b/docs/images/repository/repository-release-node-install.png new file mode 100644 index 0000000000000000000000000000000000000000..82017f22cadf0c0cd4a99420f4690741fce95375 Binary files /dev/null and b/docs/images/repository/repository-release-node-install.png differ diff --git a/docs/images/repository/repository-release-node-sources.png b/docs/images/repository/repository-release-node-sources.png new file mode 100644 index 0000000000000000000000000000000000000000..9c55178b2b4552e2ef9f23be0772fcc695dd57af Binary files /dev/null and b/docs/images/repository/repository-release-node-sources.png differ diff --git a/docs/images/repository/repository-release-nodes.png b/docs/images/repository/repository-release-nodes.png new file mode 100644 index 0000000000000000000000000000000000000000..2f280037e74d7215471dc8076cada578c5a77d1f Binary files /dev/null and b/docs/images/repository/repository-release-nodes.png differ diff --git a/docs/images/repository/repository-release-ocai-context.png b/docs/images/repository/repository-release-ocai-context.png new file mode 100644 index 0000000000000000000000000000000000000000..0ab28089b0130ab6b71aa2702df9184fdb5ee905 Binary files /dev/null and b/docs/images/repository/repository-release-ocai-context.png differ diff --git a/docs/images/repository/repository-release-ocai-result.png b/docs/images/repository/repository-release-ocai-result.png new file mode 100644 index 0000000000000000000000000000000000000000..dc841f6d1f2f2d3e22e59b649b1cd8d289104f4f Binary files /dev/null and b/docs/images/repository/repository-release-ocai-result.png differ diff --git a/docs/images/repository/repository-release-overview.png b/docs/images/repository/repository-release-overview.png new file mode 100644 index 0000000000000000000000000000000000000000..62cc26aa6ffda9fd297d0d2aafe8240d8f70689b Binary files /dev/null and b/docs/images/repository/repository-release-overview.png differ diff --git a/docs/images/repository/repository-release-packages.png b/docs/images/repository/repository-release-packages.png new file mode 100644 index 0000000000000000000000000000000000000000..7bddd74c35384c0fc2c4d1f853f209b4f0b36ecd Binary files /dev/null and b/docs/images/repository/repository-release-packages.png differ diff --git a/docs/images/repository/repository-release-private-packages.png b/docs/images/repository/repository-release-private-packages.png new file mode 100644 index 0000000000000000000000000000000000000000..ef6057d0505b61dc181f5b58bc402e0deb53e9d2 Binary files /dev/null and b/docs/images/repository/repository-release-private-packages.png differ diff --git a/docs/images/repository/repository-release-quarantine.png b/docs/images/repository/repository-release-quarantine.png new file mode 100644 index 0000000000000000000000000000000000000000..9d65a7bb5961e5b72b9cf5b2bc8e3a449f7cccbb Binary files /dev/null and b/docs/images/repository/repository-release-quarantine.png differ diff --git a/docs/images/repository/repository-release-task-failure.png b/docs/images/repository/repository-release-task-failure.png new file mode 100644 index 0000000000000000000000000000000000000000..0db70354fb0d091074c13502fa3b330c69b2977c Binary files /dev/null and b/docs/images/repository/repository-release-task-failure.png differ diff --git a/docs/images/repository/repository-release-tasks-1440.png b/docs/images/repository/repository-release-tasks-1440.png new file mode 100644 index 0000000000000000000000000000000000000000..f4efc734e9ec6e7bc1dba4fbfb211b136031e44a Binary files /dev/null and b/docs/images/repository/repository-release-tasks-1440.png differ diff --git a/docs/images/repository/repository-release-tasks.png b/docs/images/repository/repository-release-tasks.png new file mode 100644 index 0000000000000000000000000000000000000000..3b3b801c1de6d59c7473aad3bf5863e9d9ef0920 Binary files /dev/null and b/docs/images/repository/repository-release-tasks.png differ diff --git a/docs/images/repository/repository-release-upload.png b/docs/images/repository/repository-release-upload.png new file mode 100644 index 0000000000000000000000000000000000000000..5cfbd064567f5b7f665a145e1cfb9a1cb39eec60 Binary files /dev/null and b/docs/images/repository/repository-release-upload.png differ diff --git a/docs/images/repository/repository-release-upstream-form.png b/docs/images/repository/repository-release-upstream-form.png new file mode 100644 index 0000000000000000000000000000000000000000..5e705e5b8d05b9c51daba58acf498b3671053cfc Binary files /dev/null and b/docs/images/repository/repository-release-upstream-form.png differ diff --git a/docs/images/repository/repository-release-upstreams.png b/docs/images/repository/repository-release-upstreams.png new file mode 100644 index 0000000000000000000000000000000000000000..6d5324602d65fcb561a94195cbcf61e07b4c5878 Binary files /dev/null and b/docs/images/repository/repository-release-upstreams.png differ diff --git a/docs/images/repository/repository-review-20261023-command-preview.png b/docs/images/repository/repository-review-20261023-command-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..f33c742e7e80796177ddf9190fee2615168f9dbe Binary files /dev/null and b/docs/images/repository/repository-review-20261023-command-preview.png differ diff --git a/docs/images/repository/repository-review-20261023-optical.png b/docs/images/repository/repository-review-20261023-optical.png new file mode 100644 index 0000000000000000000000000000000000000000..f784020f3e1435ff04f0c6059cafc7f9119db2ef Binary files /dev/null and b/docs/images/repository/repository-review-20261023-optical.png differ diff --git a/docs/images/repository/repository-review-20261023-rerun.png b/docs/images/repository/repository-review-20261023-rerun.png new file mode 100644 index 0000000000000000000000000000000000000000..ab8edd4dfca904e70437a9916998119d88be8993 Binary files /dev/null and b/docs/images/repository/repository-review-20261023-rerun.png differ diff --git a/docs/images/repository/repository-style-20261023-distribution.png b/docs/images/repository/repository-style-20261023-distribution.png new file mode 100644 index 0000000000000000000000000000000000000000..eb0a8ed19b454972b399c98650a5e1b7ce576551 Binary files /dev/null and b/docs/images/repository/repository-style-20261023-distribution.png differ diff --git a/docs/images/repository/repository-style-20261023-list.png b/docs/images/repository/repository-style-20261023-list.png new file mode 100644 index 0000000000000000000000000000000000000000..ab135d1ef8752ac231f3b2bb4ee85525031dd6c6 Binary files /dev/null and b/docs/images/repository/repository-style-20261023-list.png differ diff --git a/docs/images/repository/repository-style-20261023-node-drawer.png b/docs/images/repository/repository-style-20261023-node-drawer.png new file mode 100644 index 0000000000000000000000000000000000000000..ec5c984d75c9fa11a27c6dbb7a91f85989e1704e Binary files /dev/null and b/docs/images/repository/repository-style-20261023-node-drawer.png differ diff --git a/docs/images/repository/repository-style-20261023-nodes.png b/docs/images/repository/repository-style-20261023-nodes.png new file mode 100644 index 0000000000000000000000000000000000000000..3ab99291812d73a52bcb40f06c9c07ecd9964152 Binary files /dev/null and b/docs/images/repository/repository-style-20261023-nodes.png differ diff --git a/docs/images/repository/repository-style-20261023-packages.png b/docs/images/repository/repository-style-20261023-packages.png new file mode 100644 index 0000000000000000000000000000000000000000..d9e9456a0ba730cdd8800d2473ac8261ae52a2fd Binary files /dev/null and b/docs/images/repository/repository-style-20261023-packages.png differ diff --git a/docs/images/repository/repository-style-20261023-sources.png b/docs/images/repository/repository-style-20261023-sources.png new file mode 100644 index 0000000000000000000000000000000000000000..fb4d94af7a43ec8b80bc67e276680b4aee42efc3 Binary files /dev/null and b/docs/images/repository/repository-style-20261023-sources.png differ diff --git a/docs/images/repository/repository-style-20261023-task-drawer.png b/docs/images/repository/repository-style-20261023-task-drawer.png new file mode 100644 index 0000000000000000000000000000000000000000..e23d6c3fe0ec61b7316fa095d37a1d7d83f0daa9 Binary files /dev/null and b/docs/images/repository/repository-style-20261023-task-drawer.png differ diff --git a/docs/images/repository/repository-style-20261023-tasks.png b/docs/images/repository/repository-style-20261023-tasks.png new file mode 100644 index 0000000000000000000000000000000000000000..ff92744ae187e05cfc7292fdcfb74359951361ef Binary files /dev/null and b/docs/images/repository/repository-style-20261023-tasks.png differ diff --git a/docs/images/repository/repository-ui-20261022-columns.png b/docs/images/repository/repository-ui-20261022-columns.png new file mode 100644 index 0000000000000000000000000000000000000000..398e59a13be2e762d0e01d376657300ed9eb1b07 Binary files /dev/null and b/docs/images/repository/repository-ui-20261022-columns.png differ diff --git a/docs/images/repository/repository-ui-20261022-distribution-detail.png b/docs/images/repository/repository-ui-20261022-distribution-detail.png new file mode 100644 index 0000000000000000000000000000000000000000..cd86626539208826d2371142c7bc660c562f5592 Binary files /dev/null and b/docs/images/repository/repository-ui-20261022-distribution-detail.png differ diff --git a/docs/images/repository/repository-ui-20261022-distribution.png b/docs/images/repository/repository-ui-20261022-distribution.png new file mode 100644 index 0000000000000000000000000000000000000000..ef1d8e8b15ef13b64edf98b5d094c2fe600e354a Binary files /dev/null and b/docs/images/repository/repository-ui-20261022-distribution.png differ diff --git a/docs/images/repository/repository-ui-20261022-install-nodes.png b/docs/images/repository/repository-ui-20261022-install-nodes.png new file mode 100644 index 0000000000000000000000000000000000000000..ec275bd0f8daec1f8049d099bb62703493427e29 Binary files /dev/null and b/docs/images/repository/repository-ui-20261022-install-nodes.png differ diff --git a/docs/images/repository/repository-ui-20261022-install-preview.png b/docs/images/repository/repository-ui-20261022-install-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..444831efbacc1d2547a8db7ed14fb9e6a0376894 Binary files /dev/null and b/docs/images/repository/repository-ui-20261022-install-preview.png differ diff --git a/docs/images/repository/repository-ui-20261022-list.png b/docs/images/repository/repository-ui-20261022-list.png new file mode 100644 index 0000000000000000000000000000000000000000..404581f56685de8fa83561d6a71367d5b9f169bf Binary files /dev/null and b/docs/images/repository/repository-ui-20261022-list.png differ diff --git a/docs/images/repository/repository-ui-20261022-ocai-context.png b/docs/images/repository/repository-ui-20261022-ocai-context.png new file mode 100644 index 0000000000000000000000000000000000000000..a002d1d060e9e354243ba5c335ef08d3fc99a2fe Binary files /dev/null and b/docs/images/repository/repository-ui-20261022-ocai-context.png differ diff --git a/docs/images/repository/repository-ui-20261022-ocai-result.png b/docs/images/repository/repository-ui-20261022-ocai-result.png new file mode 100644 index 0000000000000000000000000000000000000000..d9f6a3f5bef37c1e1e0beeb87aba2287458bfbb8 Binary files /dev/null and b/docs/images/repository/repository-ui-20261022-ocai-result.png differ diff --git a/docs/images/repository/repository-ui-20261022-task-failure.png b/docs/images/repository/repository-ui-20261022-task-failure.png new file mode 100644 index 0000000000000000000000000000000000000000..abb442c04fa90edea9fced681b97f0a2963bc1c9 Binary files /dev/null and b/docs/images/repository/repository-ui-20261022-task-failure.png differ diff --git a/docs/images/repository/repository-ui-20261022-tasks.png b/docs/images/repository/repository-ui-20261022-tasks.png new file mode 100644 index 0000000000000000000000000000000000000000..e70ad9563a6912bfe98c361c7d94a8333ada0c32 Binary files /dev/null and b/docs/images/repository/repository-ui-20261022-tasks.png differ diff --git a/docs/images/repository/repository-ui-config.png b/docs/images/repository/repository-ui-config.png new file mode 100644 index 0000000000000000000000000000000000000000..157d4b16f532ca8f6d5bae94c6ebd5c0cabf880c Binary files /dev/null and b/docs/images/repository/repository-ui-config.png differ diff --git a/docs/images/repository/repository-ui-distribution.png b/docs/images/repository/repository-ui-distribution.png new file mode 100644 index 0000000000000000000000000000000000000000..2176425c7678b379d40204b0bf6ff30d766b86e9 Binary files /dev/null and b/docs/images/repository/repository-ui-distribution.png differ diff --git a/docs/images/repository/repository-ui-list.png b/docs/images/repository/repository-ui-list.png new file mode 100644 index 0000000000000000000000000000000000000000..89497e0e7ad63426a4383c86cf7174181974efe2 Binary files /dev/null and b/docs/images/repository/repository-ui-list.png differ diff --git a/docs/images/repository/repository-ui-ocai-context.png b/docs/images/repository/repository-ui-ocai-context.png new file mode 100644 index 0000000000000000000000000000000000000000..91b58c655171a0ab81239d199de09b0da14f34b5 Binary files /dev/null and b/docs/images/repository/repository-ui-ocai-context.png differ diff --git a/docs/images/repository/repository-ui-storage-node.png b/docs/images/repository/repository-ui-storage-node.png new file mode 100644 index 0000000000000000000000000000000000000000..f6870751ef055e439958bddf1f45c422a0a03c93 Binary files /dev/null and b/docs/images/repository/repository-ui-storage-node.png differ diff --git a/docs/images/repository/repository-ui-task.png b/docs/images/repository/repository-ui-task.png new file mode 100644 index 0000000000000000000000000000000000000000..7ac03844878344cda2c02e1f45351d47a5607893 Binary files /dev/null and b/docs/images/repository/repository-ui-task.png differ diff --git "a/docs/\347\247\201\346\234\211\345\214\226\350\275\257\344\273\266\344\273\223\345\272\223\346\226\271\346\241\210\350\256\276\350\256\241.md" "b/docs/\347\247\201\346\234\211\345\214\226\350\275\257\344\273\266\344\273\223\345\272\223\346\226\271\346\241\210\350\256\276\350\256\241.md" new file mode 100644 index 0000000000000000000000000000000000000000..db91225e05482cb621db91081236ae13959050d3 --- /dev/null +++ "b/docs/\347\247\201\346\234\211\345\214\226\350\275\257\344\273\266\344\273\223\345\272\223\346\226\271\346\241\210\350\256\276\350\256\241.md" @@ -0,0 +1,1377 @@ +# OCManager 私有化软件仓库方案设计 + +| 项目 | 内容 | +| --- | --- | +| 文档版本 | V1.0 | +| 更新日期 | 2026-10-25 | +| 适用读者 | 方案评审人员、OCManager 开发与运维人员、部署和验收人员 | +| 功能基线 | `ddbdfea457644ed7d01e6c209e9dfd2168cd6953` | +| 验收基线 | OpenCloudOS 9,152 项机器验收、31 项核心与增强矩阵、9 个 Evidence Bundle 均通过 | +| 配套文档 | [部署说明](私有化软件仓库部署说明.md) · [测试验收](私有化软件仓库测试验收.md) · [用户手册](私有化软件仓库用户手册.md) · [结构化验收报告](私有化软件仓库结构化验收报告.md) | + +## 文档说明 + +### 建设背景 + +OpenCloudOS 和企业业务软件需要稳定、可追溯的软件包来源。隔离网、专有云及内部研发环境通常同时管理发行版 RPM 和业务私有 RPM;如果包文件分散保存、同步依赖临时脚本、发布目录允许直接改写,管理员就难以确认包来自哪个仓库、经过哪些校验、当前节点实际提供哪个版本。损坏包、错误架构包或未验证签名的包也可能进入客户端可用源。 + +OCManager 私有化软件仓库把上游同步、私有包上传、包签名检查、隔离处理、元数据生成、不可变 Revision、节点分发和客户端安装配置纳入同一条管理流程。OpenCloudOS 9 客户端通过标准 YUM/DNF 仓库读取 RPM 与 repodata;任务、节点、仓库内容和发布结果均可在控制台追踪。 + +### 文档范围与阅读方式 + +本文面向方案评审、开发、部署和验收人员,说明产品目标、模块落点、数据模型、接口、内容存储、发布安全、前端页面和验收设计。首次了解产品可从“方案概述”和“项目目标与范围”开始;评审实现时按“系统设计 → 数据库设计 → 接口设计 → 存储与发布设计”阅读;部署、操作和复现步骤分别见配套部署说明、用户手册与测试验收文档。 + +页面截图中的测试地址和节点标识已遮蔽,灰色区域表示隐私遮罩。部署时使用实际组织的节点地址和访问配置。历史测试日期、命令及结果以对应的验收记录为准。 + +## 目录 + +**项目定位** + +- [0. 方案概述](#0-方案概述) +- [1. 项目目标与范围](#1-项目目标与范围) + +**架构、数据与接口** + +- [2. 系统设计](#2-系统设计) +- [3. 数据库设计](#3-数据库设计) +- [4. 接口设计](#4-接口设计) + +**仓库运行与安全** + +- [5. 存储与发布设计](#5-存储与发布设计) +- [6. 任务、并发与日志设计](#6-任务并发与日志设计) +- [7. 安全与可靠性设计](#7-安全与可靠性设计) + +**控制台、部署与验收** + +- [8. 前端页面设计](#8-前端页面设计) +- [9. 部署与 OCAI 设计](#9-部署与-ocai-设计) +- [10. 验证与验收设计](#10-验证与验收设计) + +**交付、风险与实施记录** + +- [11. 预期效果](#11-预期效果) +- [12. 历史时间规划](#12-历史实施时间规划) +- [13. 验收追踪与当前状态](#13-验收追踪与当前状态) +- [14. 风险与控制措施](#14-风险与控制措施) +- [15. 最终发布门禁](#15-最终发布门禁) +- [16. 现场演示流程](#16-现场演示流程约-10-分钟) +- [17. 参考与图片说明](#17-参考与图片说明) + +**产品闭环与当前候选** + +- [18. 产品化闭环与验收关系](#18-产品化闭环与验收关系) +- [19. 审查修复候选的实现边界](#19-审查修复候选的实现边界) +- [20. Repository 界面风格统一](#20-repository-界面风格统一) + +## 0. 方案概述 + +OCManager 私有化软件仓库为隔离网、专有云和企业内部环境提供统一的 RPM 仓库管理能力。控制台管理发行版上游同步内容和业务私有软件包,按系统、版本、仓库类型与架构组织仓库,并将审核后的软件包发布为标准 YUM/DNF 源。 + +用户可在控制台完成仓库创建、上游同步、RPM 上传与校验、隔离项处理、Revision 发布、节点分发、安装配置生成和任务诊断。OpenCloudOS 9 客户端通过产品生成的 `.repo` 配置访问发布地址,并使用 DNF/YUM 安装软件包。仓库节点安装也由控制台选择 Node RPM 版本和 TMS Agent,目标节点从系统管理的 Node RPM 仓库读取指定不可变 Revision 并完成注册。 + +系统由管理面与仓库数据面组成。管理面基于 OCManager 现有 Vue 3、Footstone、MySQL、TMS/Agent 与 Compose 体系,保存仓库定义、任务、Revision 元数据、权限和日志索引。Repository Node 保存 RPM、repodata、隔离内容和原始任务日志,并通过只读 HTTP 发布仓库。TMS/Agent 承担节点命令执行和 Revision 分发;OCAI 对任务上下文和脱敏日志提供只读分析。 + +发布链路使用 Repository、Task、Mutation Lock、Worker、immutable Revision 和 Atomic Publish。远程同步和本地上传经过相同的仓库内容管理与发布流程。任务启动前和 Worker 执行前检查节点磁盘、inode、文件系统及所需工具;原始日志压缩存放在仓库节点,管理面保存结构化摘要和索引。同步配额按筛选同步和全量同步采用不同预检预算,管理员配置仍具有最高优先级。 + +安全设计覆盖项目权限、TMS HMAC 请求签名、节点 mTLS、RPM/GPG 校验、Metadata Signing、注册白名单和日志脱敏。GPG 私钥仅保存在仓库节点;Inventory 导出软件仓库定义,不包含软件包内容、历史任务或机密。OCAI 输出原因、证据和建议,不直接执行仓库变更。 + +当前验收基线在 OpenCloudOS 9 完成 152 项机器验收。2026-10-14 的全量记录、Build143 功能验收和 Build154 干净基础部署记录作为历史证据保留;当前代码、接口和操作口径以本版正文及配套文档为准。 + +## 1. 项目目标与范围 + +### 1.1 问题背景 + +隔离网和专有云环境通常同时存在发行版软件包和业务私有包。缺少统一仓库管理后,会出现以下问题: + +- RPM 分散在不同主机目录,仓库归属、产品版本和架构边界不清楚。 +- 上游同步和 `createrepo_c` 依赖人工脚本,失败后难以判断处于哪个步骤。 +- 私有包上传后缺少损坏、架构、NEVRA 和签名检查,错误包可能进入业务源。 +- 仓库内容较大,直接放在管理服务节点会占用系统盘并影响管理面稳定性。 +- `dnf install` 成功不一定代表软件包来自 OCManager 发布的仓库。 +- 同步日志很长,单纯把整段日志写入数据库或发送给 AI 都不可控。 + +### 1.2 最终功能范围 + +对外按 6 个 Core 和 3 个 Enhanced 分类展示。Enhanced 只表示分组,不表示可选;三项能力都进入最终发布门禁。 + +| 分组 | 能力 | 结果 | +| --- | --- | --- | +| Core | Repository Management | 仓库、仓库节点、产品版本、任务、包和 Revision 可管理 | +| Core | Package Ingestion | 远程同步、单 RPM/tar.gz 上传、校验、隔离和删包 | +| Core | Repository Publishing | repodata、immutable Revision、原子发布和只读 HTTP | +| Core | DNF Validation | Case A/B/C 隔离安装与量化报告 | +| Core | Automation | 配置批量化、Makefile、Shell、Compose、恢复和一键验收 | +| Core | Security | JWT/项目权限、GPG、归档防护、节点通信、日志脱敏 | +| Enhanced | Distribution | 指定 Revision 分发到一个或多个 Agent 节点并校验摘要 | +| Enhanced | Diagnosis | 规则诊断、海量日志摘要与 OCAI 智能辅助分析 | +| Enhanced | Repo Generator | 根据真实发布地址和 GPG 策略生成 `.repo` | + +### 1.3 演进基线与当前能力 + +| 范围 | 2026-08-25 历史基线 | 当前实现 | +| --- | --- | --- | +| 仓库数据位置 | 管理部署中的统一 bind mount | 数据迁移到 Repository Node 服务,manager 容器不挂载 RPM;生产环境推荐独立主机 | +| 节点管理 | 分发节点来自 TMS/Agent | 新增 Repository Storage Node 模型、心跳、容量和工具能力 | +| 容量 | 本地路径生命周期 POC 和阈值保护 | 按目标仓库节点执行在线预检,并在前端展示预测值与实际值 | +| 日志 | 任务错误、结构化规则诊断、分发输出截断 | 原始日志压缩分层存储,DB 保存索引、摘要、计数和关键窗口 | +| 智能分析 | 确定性规则引擎 | 部署 OCAI,对结构化摘要和脱敏日志进行只读分析 | +| 发布地址 | 管理 Nginx 读取统一卷 | Repository Node Nginx 直接发布,Generator 使用节点真实地址 | +| 验收 | 单机数据面 31/31 PASS | 保留核心矩阵并以 152 项固定清单执行最终验收 | + +### 1.4 交付边界 + +核心代码仍集中在: + +```text +manager/backend +manager/frontend +``` + +部署和自动化集中在: + +```text +manager/Makefile +scripts/ +manager/deployments/compose/ +config/ +Dockerfile +``` + +`tms/`、`infra/` 只有在现有链路无法满足仓库节点发现、Agent 状态或单目标/多目标分发时才做最小修改。OCAI 使用官方独立仓库部署,不把模型推理代码复制到 OCManager。 + +## 2. 系统设计 + +### 2.1 设计原则 + +1. **控制面和数据面分离**:footstone、MySQL 保留在管理服务侧;RPM、repodata、quarantine 和原始日志位于仓库节点。 +2. **仓库节点受管**:节点通过控制地址与 mTLS 身份注册并上报状态、磁盘、inode、工具和发布能力;使用控制台安装时,以 TMS Agent 为安装和白名单授权身份。 +3. **不可变发布**:任何同步、上传、删包和重建都生成新 Revision,不原地修改已发布内容。 +4. **先预检后执行**:容量、文件系统、工具、网络、架构和上游元数据不符合要求时,不创建可运行 mutation。 +5. **日志分层**:任务进度使用结构化事件;数据库保存有界摘要;原始日志压缩留存在节点。 +6. **AI 只做辅助判断**:OCAI 读取脱敏上下文并给出建议,不绕过 JWT、项目权限和 Repository Mutation Lock。 +7. **一条产品链路**:Batch、页面和自动化都调用 Repository API,不直接插库或创建仓库目录。 + +### 2.2 总体架构 + +![OCManager 私有化软件仓库总体架构](images/repository/repo-architecture.png) + +图 1:控制台、管理面、仓库节点、TMS/Agent 与 OCAI 的职责和数据流。[查看矢量图](images/repository/repo-architecture.svg)。 + +![前端入口到后端能力的功能映射](images/repository/repo-feature-map.png) + +图 2:六条主要操作路径,从页面入口对应到真实 RPC、后端模块及执行结果,说明界面操作如何复用 Repository 主链。[查看矢量图](images/repository/repo-feature-map.svg)。 + +系统划分为三个平面: + +- **管理面**:Vue 控制台、Nginx 管理入口、footstone Repository API 和 MySQL,负责鉴权、模型、任务、节点选择、状态和报告。 +- **仓库数据面**:Repository Storage Node 上的 `repository-worker`、只读 Nginx 和本地 bind data,负责同步、上传落盘、校验、repodata、发布和原始日志;该节点服务可独立部署,也可在小规模环境与管理面共用宿主机。 +- **分发与诊断面**:TMS/Agent 负责节点发现和 immutable Revision 分发;OCAI/ops-mate 负责只读智能辅助分析和可选主机诊断。 + +管理服务节点不再挂载完整 Repository 数据目录。业务 DNF 客户端访问 Repository Node 的 `publish_base_url`;管理 Nginx 只代理管理 API 和 OCAI,不承载大规模 RPM 下载流量。 + +### 2.3 模块落点 + +| 层次 | 目录/组件 | 职责 | 当前状态 | +| --- | --- | --- | --- | +| Protocol | `manager/backend/protocol/repository/`、`repositorynode/` | Repository 管理协议与节点内部任务协议 | 已实现并完成隔离代码生成验证 | +| Presentation | `manager/backend/services/footstone/presentation/` | tRPC Controller、鉴权后的请求转换 | 已实现 Repository、Node、Task、Content、Distribution、Report、OCAI Controller | +| Application | `manager/backend/services/footstone/application/` | CRUD、任务编排、节点选择、预检门禁 | 已实现控制面编排与节点任务提交闭环 | +| Domain | `manager/backend/services/footstone/domain/` | Repository、Task、Lock、Diagnosis 规则 | 已实现 Node、Capacity、Log Policy 和 Diagnosis 规则 | +| Infrastructure | `manager/backend/services/footstone/infrastructure/repository/` | MySQL、节点客户端、存储路径、TMS/OCAI 适配 | 已实现并完成 MariaDB、mTLS、TMS 与 OCAI 联调 | +| Worker | `manager/backend/cmd/repository-worker/` 与 `repositoryworker/` | 按 node ID 领取任务,执行同步、校验、发布和日志压缩 | 已在独立 Repository Node 运行并通过远程任务验证 | +| Frontend | `manager/frontend/src/pages/repository/` | 仓库、包、任务、GPG、分发、Generator、报告 | 已实现节点、容量、日志、OCAI 和全部仓库管理页面 | +| OCAI | 独立 `OpenCloudOS/ocai-service` | SSE 对话、工具编排、只读智能分析 | 已部署锁定版本并完成真实模型 SSE 联调 | +| Data Plane | Repository Node Nginx | `GET/HEAD` 发布 RPM、公钥和 repodata | 已在独立节点实现只读发布与原子 Revision 切换 | + +worker 仍采用同一 Go module 的薄入口,不把 worker 分支堆进 footstone 主入口: + +```text +manager/backend/cmd/footstone/ +manager/backend/cmd/repository-worker/ +``` + +### 2.4 仓库节点 + +Repository Storage Node 是专门保存和发布仓库内容的受管机器。节点必须满足: + +- 手工接入时已有受信控制地址、发布地址与证书;从控制台安装时目标已作为 TMS Agent 在线,并通过项目注册白名单。 +- 配置稳定的 `node_id`、内部管理地址、业务发布地址和数据根目录。 +- 本地安装 `reposync`、`createrepo_c`、`rpm/rpmkeys`、`gpg`、`tar`、`sha256sum` 和 Nginx。 +- worker 和 Nginx 共享同一宿主机 bind root;worker 为 `rw`,Nginx 为 `ro`。 +- worker 周期上报磁盘、inode、文件系统、工具版本、心跳和当前任务数。 + +一个 Repository 绑定一个主仓库节点: + +```text +Repository + -> primary_storage_node_id + -> active_revision_id + -> publish_base_url +``` + +多节点分发负责把指定 immutable Revision 复制到其他 Agent 节点。主仓库节点保存权威内容,分发目标保存可验证副本;分发目标来自现有 TMS Agent 列表,不能把“注册仓库节点”等同于“新增 TMS Agent”。 + +两类节点的注册与事实来源保持分离:Repository Storage Node 由 Footstone 的仓库节点接口创建并保存在 `repository_storage_nodes`;分发目标来自 TMS asset-manager 和 heartbeat,由 `ListRepositoryDistributionNodes -> TMS.ListNodes()` 实时读取。一台物理机可以同时承担两种角色,但不能因为已经创建仓库节点,就自动成为分发 Agent。 + +新目标节点必须安装 `ocm-agent` 并连接 TMS Gateway。TMS 允许按部署策略启用 `auto_register: true`,首次连接登记资产、分配 Agent ID,心跳上线后进入分发列表;生产部署采用 `auto_register: false` 时,由管理员先通过 TMS `/api/v1/assets/add` 预登记 UUID、IP 和 MAC。两种模式都不进行网段扫描,也不把未运行 Agent 的裸机加入分发列表。TMS 资产注册策略与 Repository Node Bootstrap 白名单分别控制两个身份阶段。 + +### 2.5 数据流 + +**远程同步** + +```text +用户创建同步任务 +-> footstone 校验项目权限和仓库状态 +-> 读取仓库节点最新容量并执行 Preflight +-> 指派 storage_node_id +-> 该节点 repository-worker 领取任务 +-> 节点直接访问 upstream +-> staging / RPM / GPG / arch 校验 +-> createrepo_c +-> immutable revision + manifest +-> 相对 current 原子切换 +-> 节点 Nginx 发布 +-> footstone 提交 active_revision_id +``` + +**本地上传** + +```text +浏览器分片上传 +-> footstone JWT/项目权限/会话校验 +-> 有界 chunk 透传至目标仓库节点 +-> 管理服务不落盘 RPM +-> 节点 staging 合并和 SHA-256 校验 +-> RPM/tar 安全校验 +-> accepted 进入新 Revision +-> rejected 进入 quarantine +-> 生成 repodata并发布 +``` + +**智能分析** + +```text +失败任务 +-> 规则引擎生成稳定错误码和结构化诊断 +-> 日志摘要器生成脱敏关键窗口、重复计数和阶段指标 +-> BuildRepositoryOCAIContext 生成有界上下文 +-> OCAI SSE 对话返回原因解释与建议 +-> 用户回到 Repository 页面选择重试/重新同步/重建元数据 +-> 操作结果作为反馈追加到 OCAI 会话 +``` + +## 3. 数据库设计 + +### 3.1 数据边界 + +MySQL 保存可查询、可关联、需要事务保护的元数据,不保存 RPM、repodata、原始大日志、GPG 私钥或完整公钥正文。 + +```text +MySQL: Repository / Node / Revision / Package metadata / Task / Lock / + Preflight / Log index / Diagnosis / Distribution / GPG metadata + +Repository Node: RPM / repodata / staging / quarantine / compressed raw logs / + published public keys / trash +``` + +### 3.2 现有领域模型 + +![Repository 领域模型](images/repository/repo-er-model.png) + +图 3:仓库内容、节点观测、任务目标、上传分片、安装授权和签名等领域关系。[查看矢量图](images/repository/repo-er-model.svg)。 + +上图按领域聚合主要关系,包含任务目标、分片、Schedule、安装授权和签名;精确字段与约束以本节表格及 Migration 为准,不将所有逻辑关联都视为物理外键。现有主要表如下: + +| 表 | 作用 | +| --- | --- | +| `software_repositories` | 仓库定义、产品版本、架构、上游、策略、发布标识和 active Revision | +| `repository_upstream_profiles` | 项目自定义上游地址模板、适用版本/架构和默认过滤规则 | +| `repository_tasks` | 异步任务、lease、fencing token、进度、错误和规则诊断 | +| `repository_mutation_locks` | 同 Repository 变更串行化 | +| `repository_uploads` | 分片上传会话和完整性状态 | +| `repository_revisions` | 不可变快照、包数量、容量和摘要 | +| `repository_packages` | Revision 内包的 NEVRA、架构、来源和 SHA-256 | +| `repository_quarantine_items` | 被拒绝内容、原因、摘要和存储相对路径 | +| `repository_distributions` | 固定 Revision 的分发任务 | +| `repository_distribution_targets` | 每个 Agent 目标的状态和输出索引 | +| `repository_gpg_keys` | key ID、fingerprint、公钥路径和创建时间 | +| `repository_node_repository_usages` | 节点观测的逐仓库物理占用与观测时间 | +| `repository_task_targets` | 一次业务任务中各 Node/Agent 的独立状态、进度与结果 | +| `repository_task_result_items` | 单次批量删除中每个包的执行结果 | +| `repository_sync_schedules` / `repository_schedule_runs` | 定时规则、失败暂停及去重调度事件 | +| `repository_upload_chunks` | Indexed Chunk 的唯一事实来源 | +| `repository_operation_previews` | 删除、安装和导入的短期 Preview、Digest 与资源版本 | +| `repository_package_installs` | 多目标安装预演和执行的领域数据 | +| `repository_node_enrollments` / `repository_node_bootstrap_tokens` | 安装前身份、一次性授权及精确恢复状态 | +| `repository_node_certificates` | 唯一证书结果来源,只保存公开证书与元数据 | +| `repository_node_artifacts` / `repository_node_install_sources` | Node RPM 的版本、架构、标准源、包与 immutable Revision 引用 | +| `repository_node_registration_allowlists` | 按项目、TMS Agent 控制首次注册及重新安装 | +| `repository_metadata_signing_keys` | 节点上报的 Metadata Signing Key Inventory,不含私钥 | +| `repository_inventory_import_jobs` | 导入预演、映射、逐项结果及统一任务引用 | + +OpenCloudOS BaseOS/AppStream 内置来源属于前端产品默认值,不写入数据库;`repository_upstream_profiles` 只保存项目自定义来源。数据库不把 Repository 与 profile 强绑定,选择来源时仍然生成 `software_repositories.upstream_url` 和过滤规则快照,避免配置生命周期影响已发布仓库。 + +### 3.3 仓库节点与占用模型 + +`repository_storage_nodes` 保存节点身份、连接配置和最新观测。以下使用实际协议字段说明对外数据: + +| 字段 | 类型/约束 | 说明 | +| --- | --- | --- | +| `id` | `char(36)` PK | 仓库节点 ID | +| `project_id` | `bigint`, index | 所属项目;平台共享节点使用明确的平台作用域 | +| `name` | `varchar(128)` | 页面显示名称 | +| `control_base_url` / `publish_base_url` | URL | 内部控制地址与对外源根地址 | +| `tls_server_name` / `certificate_fingerprint` | 字符串 | mTLS 校验身份 | +| `status` / `enabled` | 枚举/布尔 | 在线状态与管理员启停状态分别表达 | +| `hostname` / `ip_address` | 字符串 | 节点身份与显示信息 | +| `architecture` / `os_name` / `os_version` / `agent_version` | 字符串 | 架构、系统与 Node 版本 | +| `filesystem_type` | 字符串 | xfs/ext4 等 | +| `capacity_bytes` / `free_bytes` / `free_inodes` | 字节/数量 | 节点文件系统观测 | +| `disk_used_bytes` / `disk_usage_percent` | 聚合字段 | 后端按文件系统观测计算 | +| `repository_count` / `repository_physical_bytes` | 聚合字段 | 后端关联仓库与 Node Usage 统计 | +| `tool_versions` | map | reposync、createrepo_c、rpm、gpg 等版本 | +| `last_heartbeat_at` / `last_probe_at` | Timestamp | 心跳与探测时间 | +| `last_error_code` / `last_error_message` | 字符串 | 技术排障信息;响应另给统一中文说明与建议 | +| `created_at`/`updated_at` | `datetime(3)` | 审计时间 | + +`software_repositories` 增加: + +```text +primary_storage_node_id +publish_base_url_snapshot +``` + +Repository 创建后不能在有运行任务时直接切换主节点。节点迁移使用绑定 active Revision 的专用任务,校验 manifest 后再切换,不自动跨节点再平衡。 + +`DescribeRepositoryStorageNode` 同时返回 `repositories[]`:仓库 ID、名称、系统/版本、类型、架构、上游、`package_count`、`logical_bytes`、`physical_bytes`、`current_revision`、发布状态、最近/下次同步、运行任务 ID 与最近同步错误。逻辑大小来自 active Revision 的 RPM 内容;物理占用来自 `repository_node_repository_usages`,包含该仓库的 revisions、staging、quarantine、metadata、trash。两者不能互相替代,节点磁盘总量也不能由仓库逻辑大小相加获得。 + +节点列表的节点数、在线数、总容量、已用容量和高风险节点数,由后端按当前项目及有效筛选条件的完整集合统计,独立于当前分页。 + +### 3.4 容量预检模型 + +新增 `repository_preflight_records`: + +| 字段 | 说明 | +| --- | --- | +| `id/task_id/repository_id/storage_node_id` | 关联任务、仓库和节点 | +| `operation` | `SYNC/UPLOAD/DELETE/REBUILD/PUBLISH/DISTRIBUTE` | +| `estimated_source_bytes` | 从上游 metadata 或上传声明得到的输入规模 | +| `active_revision_bytes` | 当前活动 Revision 大小 | +| `projected_revision_bytes` | 新 Revision 预计大小 | +| `metadata_reserve_bytes` | repodata、manifest 和临时文件预留 | +| `safety_margin_bytes` | 默认 20% 安全余量 | +| `required_free_bytes` | 本次任务要求的可用空间 | +| `actual_free_bytes/inodes_free` | 预检时节点实际值 | +| `fs_usage_percent` | 文件系统使用率 | +| `tool_check/network_check/fs_check` | 分项结果 | +| `status/reason_code` | `PASS/FAIL/WARN` 与稳定失败码 | +| `measured_at` | 预检时间 | + +Preflight 是任务 Evidence 的一部分,不能只在页面临时计算。 + +### 3.5 日志索引模型 + +新增 `repository_task_log_indexes`,数据库只保存: + +```text +id +task_id +storage_node_id +log_uri +codec +raw_bytes +compressed_bytes +line_count +sha256 +first_event_at +last_event_at +retained_until +excerpt_json +pattern_counts_json +redaction_version +created_at +``` + +`excerpt_json` 只包含有界关键窗口,不包含整段日志。建议限制: + +- 每个失败步骤最多 20 条代表日志。 +- 每条最多 2 KiB。 +- 任务详情默认返回不超过 64 KiB 的摘要。 +- OCAI 输入默认不超过 32 KiB,超出后按错误优先级、步骤和时间窗口截断。 + +### 3.6 OCAI 关联元数据 + +新增 `repository_ai_analyses`,只保存 OCAI 会话关联,不保存模型密钥或完整 prompt: + +```text +id +task_id +ocai_session_id +context_sha256 +context_version +status +requested_by +created_at +updated_at +``` + +OCAI 自身负责会话和消息存储;Repository 侧保留 task 与 session 的可追溯关系。 + +### 3.7 约束与索引 + +- Repository 名称唯一范围:`project_id + system_product + product_version + name + architecture`。 +- 每个 Revision 内 NEVRA 唯一:`revision_id + nevra`。 +- 同一仓库同时只允许一个 mutation lock owner。 +- 任务领取索引增加 `storage_node_id + status + available_at`,确保各节点只领取分配给自己的任务。 +- Node 安装授权唯一范围为 `(project_id, tms_agent_id)`;安装源唯一范围为 `(project_id, architecture)`。 +- 日志索引按 `task_id` 唯一,按 `retained_until` 建清理索引。 +- GPG 表只保存 `id/repository_id/key_id/fingerprint/public_key_path/created_at`,不保存公钥正文和任何私钥。 + +### 3.8 任务、上传与发布约束 + +| 模型 | 数据约束 | +| --- | --- | +| Task Target | `UNIQUE(task_id, target_type, target_id)`;整体 Task 聚合目标状态,Target 保存独立结果 | +| Upload Chunk | `UNIQUE(upload_id, chunk_index)`;同 SHA-256 重传幂等,不同 SHA-256 拒绝;uploaded count/bytes 是可重建聚合值 | +| Schedule Run | 唯一 `schedule_run_key`,任务使用唯一 `idempotency_key`,多 Scheduler 只能创建一个业务任务 | +| Preview | 绑定创建人、有效期、`preview_digest` 与资源版本;Confirm 重新验证 | +| Batch Delete | `UNIQUE(task_id, item_key)`;一次任务逐包保存结果,只生成一次新 Revision/repodata | +| Inventory Import | Job 的 `repository_task_id` 唯一关联统一任务,Job 保存领域映射而不建立第二个全局状态机 | +| Enrollment/Certificate | `authorization_id` 唯一关联授权与公开证书结果;Node 私钥不入库 | + +### 3.9 数据库升级 + +当前增量按职责分为 v15(节点可观测性、同步策略、Schedule)、v16(Target、Enrollment/Certificate、Indexed Chunk、Preview 和 Package Install)、v17(GPG、Metadata Signing、Inventory Import)、v18(Node RPM 系统源、白名单、授权恢复、逐包结果与 Import Task 关联)。v18 已按完整 Schema 冻结,不回改已应用迁移。部署按既有 Migration 框架检查已应用记录,并验证空库安装、v17 升级、旧数据兼容及重入检测;新增需求需使用后续 Migration。 + +## 4. 接口设计 + +### 4.1 接口标准 + +Repository 管理接口遵循项目现有 tRPC JSON HTTP 规范: + +| 项目 | 标准 | +| --- | --- | +| 服务前缀 | `/trpc.ocmanager.repository.RepositoryService` | +| HTTP 方法 | 管理 RPC 统一使用 `POST` | +| Content-Type | `application/json` | +| 认证 | 使用登录时返回的动态 JWT header;前端复用现有 `fetchApi` | +| 项目权限 | Controller/Application 校验 `project_id` 和 Repository 所属项目 | +| 前端模块 | `fetchApi` 自动注入 `module_id` | +| 分页 | `common.PageRequest{page_size,page_number,order,order_by}` | +| 时间 | protobuf Timestamp,经 JSON 输出 RFC 3339 | +| 大整数 | 容量统一使用 bytes,前端负责 IEC 单位展示 | +| 幂等 | 按接口使用 `idempotency_key`、Schedule Run Key、Preview Digest 或 Chunk 唯一键;不假定所有创建接口都有 `request_id` | +| 错误 | HTTP 可达不代表业务成功,前端根据统一 envelope 判断 | + +成功响应: + +```json +{ + "success": true, + "error_code": "", + "return_code": 0, + "error_message": "", + "data": {"repository": {"id": "...", "name": "oc9-base"}} +} +``` + +失败响应: + +```json +{ + "success": false, + "error_code": "REPOSITORY_NODE_INSUFFICIENT_SPACE", + "return_code": 400123, + "error_message": "仓库节点可用空间不足,任务未启动", + "data": null +} +``` + +API 保留稳定字段名和技术错误码供集成与排障使用;Token、私钥、数据库凭据和含认证信息的命令不得出现在响应或下载日志中。 + +### 4.2 已实现管理 RPC + +以下接口已经存在于 `repository.proto`,URL 均为“服务前缀 + `/RPC`”。 + +| 领域 | RPC | 主要输入 | 主要输出 | 页面 | +| --- | --- | --- | --- | --- | +| 仓库 | `Create/Update/Describe/List/DeleteRepository` | RepositorySpec、ID、筛选和分页 | Repository | 仓库管理/详情 | +| 上游源 | `Create/Update/Describe/List/DeleteRepositoryUpstreamProfile` | profile spec、ID、关键字和分页 | Upstream Profile | 上游源/创建仓库 | +| 批量 | `BatchApplyRepositories` | Repository items | 逐项结果 | 配置初始化 | +| 同步 | `SyncRepository` | repository ID | RepositoryTask | 仓库详情 | +| 配置 | `GenerateRepositoryConfig` | ID + immutable flag | `.repo` 内容 | Repo 配置 | +| GPG | `Register/List/DeleteRepositoryGPGKey` | ID、公钥或 key ID | 公钥元数据 | GPG 公钥 | +| 任务 | `Describe/List/Cancel/RetryRepositoryTask` | task/repository/status/page | Task | 任务页签/详情 | +| 诊断 | `ExportRepositoryTaskDiagnosis` | task ID | JSON + SHA-256 | 任务详情 | +| 上传 | `BeginRepositoryUpload`、`PutRepositoryUploadChunk`、`DescribeRepositoryUploadChunks`、`Pause/Resume/Complete/Abort/DescribeRepositoryUpload` | 会话、chunk_index、SHA-256、fingerprint | Upload/缺失分片/Task | 软件包中的上传队列 | +| 包 | `ListRepositoryPackages` | repo/revision/name/arch/page | 包列表 | 软件包 | +| 隔离 | `ListRepositoryQuarantine` | repo/task/reason/page | 隔离项 | 隔离区 | +| 包 | `DeleteRepositoryPackage` | repo ID + NEVRA | Task | 软件包 | +| 批量删除 | `PreviewRepositoryPackageDeletion`、`ConfirmRepositoryPackageDeletion` | NEVRA 集合、Revision、Preview/Digest | 依赖影响/单个删除 Task | 软件包 | +| 软件包安装 | `PreviewRepositoryPackageInstall`、`ConfirmRepositoryPackageInstall`、`DescribeRepositoryPackageInstall` | 包、immutable Revision、目标 Agent、Preview/Digest | 各 Target 结构化交易和结果 | 软件包/任务详情 | +| 定时同步 | `Upsert/Describe/Pause/ResumeRepositorySyncSchedule` | 仓库、周期、时区、失败阈值 | Schedule/下次运行 | 仓库概览 | +| 分发 | `ListRepositoryDistributionNodes` | 无 | 在线 Agent | 节点分发 | +| 分发 | `Create/Describe/ListRepositoryDistribution` | repo/revision/targets | Distribution | 节点分发 | +| 导出 | `ExportRepositoryInventory` | project ID | JSON + SHA-256 | 仓库列表 | +| 导入 | `PreviewRepositoryInventoryImport`、`ConfirmRepositoryInventoryImport` | 清单、节点/Key 映射、冲突策略、Preview/Digest | 逐项预演/Import Task | 仓库列表 | +| 导出 | `ExportRepositoryValidationReport` | repository ID | JSON + SHA-256 | 仓库详情 | +| 导出 | `ExportRepositoryDistributionResult` | distribution ID | JSON + SHA-256 | 分发详情 | + +### 4.3 节点、日志与安装管理 RPC + +以下均为当前 `RepositoryService` 实际接口;精确字段、枚举与校验规则以 `manager/backend/protocol/repository/repository.proto` 为准。 + +| RPC | 主要输入 | 主要输出 | 认证/幂等 | 前端位置 | +| --- | --- | --- | --- | --- | +| `CreateRepositoryStorageNode` | project、name、control/publish URL、TLS server name、fingerprint | Node | JWT | 仓库节点 | +| `UpdateRepositoryStorageNode` | node ID + 可编辑配置 | Node | JWT | 节点详情 | +| `DescribeRepositoryStorageNode` | node ID | Node、Repository Summary、签名 Key Inventory | JWT | 节点 Drawer | +| `ListRepositoryStorageNodes` | project/status/keyword/page | Node list + 完整筛选集统计 | JWT | 仓库节点 | +| `DeleteRepositoryStorageNode` | node ID + confirm | Empty | JWT;有仓库绑定时拒绝 | 仓库节点 | +| `ProbeRepositoryStorageNode` | node ID | 探测结果 | JWT | 节点/创建表单 | +| `ListRepositoryPreflightRecords` | node/repository 与分页 | 分项预检记录 | JWT | 节点 Drawer | +| `DescribeRepositoryTaskLog` | task ID | 摘要、计数、关键窗口 | JWT | 任务详情 | +| `DownloadRepositoryTaskLog` | task ID | 后端脱敏 tar.gz 日志 Bundle | JWT | 任务详情 | +| `BuildRepositoryOCAIContext` | task ID | 脱敏有界上下文 | JWT | 智能分析 | +| `CreateRepositoryOCAIAnalysis` / `DescribeRepositoryOCAIAnalysis` | task ID/analysis ID | OCAI 会话、状态和关联信息 | JWT | 智能分析 | +| `AppendRepositoryOCAIFeedback` | session ID、action task ID、action result | 会话关联状态 | JWT | 任务详情 | +| `Configure/ListRepositoryNodeInstallSource(s)` | 项目、架构、系统 Node RPM Repository | 安装源配置 | JWT | 仓库节点 | +| `Register/ListRepositoryNodeArtifact(s)` | Repository、Revision、包或查询条件 | NEVRA、架构、SHA-256、immutable URL | JWT | 安装节点版本选择 | +| `Upsert/ListRepositoryNodeRegistrationAllowlist(s)` | 项目、Agent、架构、有效期 | 授权记录 | JWT | 仓库节点 | +| `CreateRepositoryNodeEnrollment` | Agent、Artifact、节点安装配置 | Pending Node/Enrollment/Task | JWT + 白名单 | 安装仓库节点 | + +`RepositorySpec` 包含 `primary_storage_node_id`。创建时后端校验节点属于当前项目且已启用;建议在表单操作前探测节点并确认容量。真正的 mutation 在创建 Task 和执行前校验在线状态、预检有效期及最新容量,不能把“仓库定义创建成功”当成“容量检查已通过”。 + +上游源配置只保存可复用模板和默认规则。创建仓库时,前端将其解析为 `RepositorySpec.upstream_url`、`include_packages` 和 `exclude_packages`;Repository 不依赖 profile ID。这样后续修改来源配置不会改变已有仓库,远程同步和 worker 继续使用原有协议。 + +### 4.4 仓库节点内部协议 + +浏览器不能直接访问节点内部管理接口。footstone 与 repository-worker 之间使用内网 HTTPS + mTLS。节点领取任务、报告心跳并通过准备/确认提交与控制面协调: + +```text +Node → Manager RepositoryNodeInternalService: + HeartbeatNode / ClaimTask / HeartbeatTask / ReportTaskFailure + PrepareTaskCommit / ConfirmTaskCommit / ExecuteDistributionTask + RenewNodeCertificate + +初次身份建立 RepositoryNodeBootstrapService: + BootstrapNode +``` + +要求: + +- token 只能绑定 node ID、upload/task ID、操作和过期时间。 +- Indexed Chunk 按 `upload_id + chunk_index` 定位并校验长度和 SHA-256;旧顺序 Append 仅保留兼容,不能成为新版断点续传的第二份事实来源。 +- 内部接口不接受用户指定任意绝对路径。 +- 原始日志下载先经过 footstone 权限检查,再返回短期地址或由 API 流式转发。 +- 不在 MySQL、Git、公开 `.env` 中保存节点私钥。 + +Manager 向 Node 的 Probe、分片传输、ActivateRevision 和日志读取使用节点现有 HTTPS 管理接口;上面列出的 tRPC 方法与该方向的 HTTP 接口分别有明确职责,不把示意操作名当成新增公共 RPC。 + +### 4.5 Inventory 导入/导出约定 + +导出 `ocmanager.repository.inventory/v2` 表达 Repository 定义与配置,不是完整备份;兼容读取 v1。清单可以包含逻辑节点引用和 `key_ref`,不能包含私钥、Bootstrap Token、TMS/OCAI 凭据、数据库密码或临时认证材料,也不恢复 RPM、Revision 内容和 Task History。 + +导入先调用 `PreviewRepositoryInventoryImport`,由用户映射当前项目的 Node/Key 并选择冲突策略,逐项展示 CREATE/SKIP/CONFLICT/INVALID。Confirm 校验 `preview_id`、Digest、创建人、有效期和资源版本,创建统一 `INVENTORY_IMPORT` Task;Job 保存领域结果和对象映射,通过 `repository_task_id` 关联任务。运行状态、开始结束时间、进度和错误以 Task 为权威来源,任务总览可以直接查询。 + +### 4.6 容量预检示例 + +```http +POST /trpc.ocmanager.repository.RepositoryService/ProbeRepositoryStorageNode +Content-Type: application/json +: +``` + +```json +{ + "storage_node_id": "node-oc9-repo-a", + "repository_id": "repo-oc9-base", + "operation": "SYNC", + "estimated_source_bytes": 536870912000, + "request_id": "preflight-20260828-001" +} +``` + +响应中的 `preflight` 至少包含 `status/total_bytes/available_bytes/required_free_bytes/projected_usage_percent/inodes_free/fs_type/rename_supported/relative_symlink_supported/tool_check/measured_at`。 + +### 4.7 日志接口标准 + +大日志不得通过普通 JSON 接口整体返回,也不得把 `AppendRepositoryUploadChunk.data`、公钥正文或完整 command output 写入通用请求日志。Repository 相关中间件只记录: + +```text +request_id +task_id/upload_id +repository_id +storage_node_id +offset +chunk_length +chunk_sha256 +result +duration +``` + +用户明确下载原始日志时,使用 JWT 保护的流式下载,并设置: + +```text +Content-Type: application/zstd 或 text/plain +Content-Disposition: attachment +ETag: +Cache-Control: private, no-store +``` + +## 5. 存储与发布设计 + +### 5.1 节点目录 + +每个 Repository Storage Node 默认使用宿主机 bind mount: + +```yaml +${REPOSITORY_DATA_HOST_DIR:-/opt/ocmanager/repository-data}: + /var/lib/ocmanager/repository-data +``` + +节点内部路径固定: + +```text +repository-data/ +├── staging// +├── quarantine/// +├── revisions-private/// +├── task-logs/YYYY/MM// +├── keys///public.key +├── trash/// +└── public// + ├── revisions// + ├── keys//public.key + └── current -> revisions/ +``` + +Repository Node worker 使用 `rw`,节点 Nginx 只挂载 `public/` 为 `ro`。私钥不得进入该目录。 + +### 5.2 远程同步落盘 + +同步任务先执行节点容量门禁。设置 include 时,worker 使用隔离的 DNF 配置,把包名/glob、exclude 和目标架构直接交给 `dnf download`,因此筛选在网络下载前生效;未设置 include 时才使用 `dnf reposync` 获取完整快照。所有下载结果仍会再次执行 RPM 信息、架构、include/exclude、checksum 和 GPG 校验,避免绕过发布前检查。完整同步必须满足节点容量和任务上限,不建议在容量不足的节点上直接同步整个发行版源。 + +过滤规则在上游元数据中不存在时不再导致整次同步失败:节点 DNF 配置使用非严格模式(`strict=0`),已存在的包正常下载并发布,任务仍为 `SUCCESS`;未匹配的包名汇总为一条 `WARN` 任务日志(字段 `unmatched_packages`),可在任务详情中核对。全部未匹配时任务以 `SYNC_EMPTY_RESULT` 失败,错误信息中列出未匹配的包名,便于直接修正过滤规则后重试。 + +OpenCloudOS 9.6 的内置来源同时覆盖 `x86_64`、`aarch64` 和 `noarch` 三种仓库架构。`x86_64` 与 `aarch64` 分别使用上游对应架构的 BaseOS/AppStream 元数据;`noarch` 不是一套独立发行版目录,因此使用已核验的 `x86_64` 元数据入口进行候选查询,再由下载参数和发布前 RPM 校验双重限制为 `noarch`。同一 noarch 包从 x86_64、aarch64 元数据入口取得的 SHA-256 已做一致性验证,避免把平台相关 RPM 混入 noarch 仓库。前端内置来源在此基础上同时提供 `mirrors.opencloudos.tech` 国内镜像的 BaseOS/AppStream 两项,通道、支持版本、架构和包过滤预设与官方源逐项对应。 + +预检有效期为 5 分钟。任务排队超过有效期时,领取阶段不会沿用失效环境数据,也不会仅因排队超时直接失败;系统保留任务创建时的容量估算,结合节点最新心跳、磁盘、inode、文件系统和工具信息生成新的预检记录。最新条件不满足时仍按稳定错误码拒绝任务,满足时更新任务绑定的 `preflight_id` 后继续执行。 + +Remote Sync 采用 snapshot 语义,只允许 `SUCCESS` 或 `FAILED`。任一关键 RPM 下载失败、checksum 不一致、架构不符、RPM/GPG 校验失败、metadata 生成失败或 publish 失败,整次 Revision 不发布,active Revision 保持不变。 + +同步模式另分 `MIRROR` 和 `APPEND`:前者的新 Revision 反映本次上游选择结果,处理上游已删除的包;后者保留已有内容并追加本次有效包。两种模式均先完成整个新 Revision 再切换,不改变失败不发布原则。`bandwidth_limit_bytes` 与 `max_parallel_downloads` 写入任务实际配置并传递到 DNF,验收记录真实速率及下载并发,不能只验证 API 字段能保存。 + +每个仓库最多启用一个定时规则,支持每日/每周、时区及连续失败阈值。Schedule 状态为 `ENABLED/PAUSED/AUTO_PAUSED`;达到阈值自动暂停,用户恢复时按规则重置计数。调度事件使用唯一 Run Key 和任务幂等键防止多实例重复触发;已有 Sync 时记录 `SKIPPED_ALREADY_RUNNING`,不创建失败任务、不增加失败次数。 + +### 5.3 上传落盘 + +上传使用 Indexed Chunk 会话,单片上限 4 MiB。管理 API 校验项目、上传者、chunk index、长度和 SHA-256 后,将分片传给目标节点,管理服务不创建 RPM 临时文件。分片可乱序;相同分片和摘要重传幂等,冲突摘要拒绝。完成时检查全部分片、合并并计算服务端整文件 SHA-256,再进入原有 RPM/归档校验和发布链路。 + +多文件队列每个文件对应独立 Session,默认最多两个文件同时传输;总进度按已上传字节/总字节计算。网络中断、Pause/Resume 后只补缺失分片。页面刷新可以恢复服务端会话信息,但浏览器不能自动恢复普通 `File` 对象时需重新选择同一文件,通过 fingerprint 匹配会话继续传输,不从零重传。 + +单 RPM 必须全部通过才生成新 Revision。tar.gz 允许 `PARTIAL_SUCCESS`:合法包进入新 Revision,不合法包进入 quarantine,任务详情分别列出 accepted/rejected 和原因。危险归档直接失败,包括绝对路径、路径穿越、symlink/hardlink 逃逸、异常展开大小、异常文件数、压缩炸弹和未解决的 NEVRA 冲突。 + +上传在完成摄取前失败(GPG 预检、无包变更、归档异常、任务取消等)时,控制面会把对应 upload 记录统一推进到 `FAILED` 并写入 error_code/error_message,不再留下不可见的 `READY` 残留;仓库删除门禁只把未过期的上传计入保护,`expires_at` 已过期的上传不再阻断删除。 + +### 5.4 容量预检 + +保守容量公式为: + +```text +required_free = + active_revision_size + + max_import_expanded_size + + projected_new_revision_size + + metadata_reserve + + safety_margin +``` + +默认 `safety_margin=20%`,文件系统使用率硬阈值为 90%。达到硬阈值时拒绝新的 mutation,但不影响已发布仓库的读取。 + +| 操作 | 估算方法 | +| --- | --- | +| Remote Sync | Include 非空按筛选预算估算,默认 2 GiB/1000 包;全量按 64 GiB/100000 包预算估算,管理员配置优先,执行阶段另校验实际接收量 | +| 单 RPM Upload | 使用客户端声明大小并在完成后核对实际大小 | +| tar.gz Upload | 使用压缩包大小、展开上限和归档索引计算保守值 | +| Delete Package | 保守保留当前 Revision 全量大小,再计算新 Revision、metadata 和切换预留,不提前假定删包已释放磁盘 | +| Rebuild Metadata | 当前 Revision 大小加 metadata reserve | +| Distribution | manifest 中全文件大小加 staging 和安全余量 | + +上述 Sync 数值是保守预算,不是已经解析出的上游精确大小。任务同时受管理员硬上限和实际文件系统门禁约束,不能以未知大小绕过预检。 + +页面同时展示节点磁盘总量、已用/可用空间、inode、当前 Revision、预计输入、新 Revision、发布峰值、安全余量、预计使用率和 PASS/WARN/FAIL 原因。 + +### 5.5 repodata 与原子发布 + +![repodata 与原子发布流程](images/repository/repo-publish-flow.png) + +图 4:从内容校验、repodata 和可选元数据签名,到 Prepare、节点 Activate/Switch、Confirm、相对链接切换及异常对账。[查看矢量图](images/repository/repo-publish-flow.svg)。 + +发布顺序: + +1. staging 中完成 RPM、架构、NEVRA、checksum 和 GPG 校验。 +2. 使用 `createrepo_c --checksum sha256` 生成标准 repodata。 +3. 生成 package manifest、snapshot manifest、`repomd_sha256`。 + 启用 Metadata Signing 时,由节点本地密钥签署 `repomd.xml`,同时固化签名和公开公钥引用。 +4. 将目录固化到 `public//revisions//`。 +5. Worker 调用 `PrepareTaskCommit`;Footstone 校验 task lease、fencing token、取消状态和 Mutation Lock,核验结果对象后在事务中导入 Revision/Package/Quarantine 元数据,记录 PREPARED。 +6. 收到 Prepare 确认后,节点创建相对 symlink 临时文件,通过同文件系统 `rename` 原子切换 `current`,校验链接目标和 `repomd.xml`。 +7. Worker 调用 `ConfirmTaskCommit`,Footstone 核对提交身份后在事务中更新 `active_revision_id`、任务终态和锁;HTTP/DNF 验证使用真实发布结果。 + +文件系统切换和数据库提交不构成同一个原子事务。链接切换后若 Confirm 响应丢失,应依据 PREPARED、current、Revision 和 manifest 进行确认重试或恢复对账,不能仅凭网络错误判定已经回滚。 + +分发、验证和恢复均绑定 `repository_id + revision_id + manifest_sha256 + repomd_sha256 + storage_node_id`。 + +### 5.6 HTTP 发布 + +Repository Node Nginx 只允许 `GET/HEAD`,拒绝 `POST/PUT/PATCH/DELETE`,并设置 `autoindex off`。上传只能走 JWT 保护的管理 API。 + +```text +稳定地址:/repositories//current/ +固定版本:/repositories//revisions// +公钥地址:/repositories//keys//public.key +``` + +`.repo` Generator 必须从 Repository 绑定节点的真实 `publish_base_url` 生成配置。 + +语义化路径是兼容 Alias:`/repositories/////current/`。Slug 与显示名称分离,创建后保持稳定;修改 Display Name 不迁移 publish key、主键或历史 Revision,旧 publish-key URL 继续有效。 + +## 6. 任务、并发与日志设计 + +### 6.1 异步任务 + +![Repository 异步任务状态](images/repository/repo-task-state.png) + +图 5:统一 Task、Target 和逐项结果的状态关系,以及取消、重试、lease/fencing 和发布边界。[查看矢量图](images/repository/repo-task-state.svg)。 + +同步、上传完成处理、删包、重建、发布、验证和分发都以任务执行。API 只创建任务并返回 task ID,worker 按 `storage_node_id` 领取任务、续租并上报进度。 + +```text +PENDING +RUNNING +CANCEL_REQUESTED +CANCELED +SUCCESS +FAILED +PARTIAL_SUCCESS +``` + +Retry 创建新的 task,保留 `root_task_id` 与 `retry_of_task_id`,不复用原任务记录。 + +统一任务还覆盖 `NODE_INSTALL`、`PACKAGE_INSTALL` 和 `INVENTORY_IMPORT`。一次多节点操作只创建一个业务 Task,目标结果保存在 Task Target;前端从 Task 展开各目标的状态、错误和结果,失败目标优先显示。全局任务支持仓库、节点、类型、触发来源、状态和时间筛选;时间范围统一按 `created_at`,UTC/RFC3339 传输。顶部四态统计受项目及其它有效筛选影响,独立于当前 status filter 和分页。 + +Package Install 先对各 Agent 生成结构化交易预演,返回所选包、依赖、升级/替换包、下载量、磁盘变化和原始输出;解析在后端完成并固定 `LC_ALL=C`、`LANG=C`。Preview、Confirm 和执行始终绑定同一 immutable Revision URL,只有 READY Target 能确认。 + +批量删除先计算绑定 Revision 内直接/传递反向依赖,禁用全部外部默认源并保留依赖原因。Confirm 校验 Preview 的创建人、Digest、Revision 和有效期,资源变化要求重新预演;默认取消,不静默级联。一个删除 Task 保存逐包 Result Item,最后只重建一次 repodata。 + +### 6.2 Repository Mutation Lock + +同一 Repository 的 sync、upload publish、delete package、rebuild metadata、delete repository、publish 和 storage node migration 不得并发。不同 Repository 可以并发。Validation 可以与 mutation 并行,但必须固定读取启动时绑定的 revision ID;Distribution 同样绑定 immutable revision,不读取 `current`。 + +锁采用 MySQL lease + fencing token。旧 worker 即使在超时后恢复,也无法提交比当前 token 更旧的 Revision。 + +### 6.3 日志采集与关键内容提取 + +worker 对每个外部命令同时生成两类输出: + +1. **原始日志流**:stdout/stderr 带时间戳写入 JSONL,并在写入过程中流式 zstd 压缩;结束时收尾并记录 SHA-256、大小和行数,不把整份日志先积累在内存中。 +2. **结构化事件流**:边执行边解析,写入当前步骤、进度、计数、稳定错误码和必要证据。 + +结构化事件至少包含 `timestamp/task_id/repository_id/storage_node_id/step/severity/event_code/package_nevra/bytes_done/bytes_total/message_template/redacted_fields`。 + +关键提取规则: + +- 保存第一个错误、最终错误和每个失败步骤前后各 N 行。 +- 相同错误按 fingerprint 合并,保存 count、first_seen、last_seen 和一条代表样本。 +- HTTP/DNS、checksum、arch、GPG、dependency、磁盘和 inode 错误使用稳定分类器。 +- 高频成功行只累计数量,不逐行写入数据库。 +- URL userinfo、Authorization、Bearer、password、token、secret、API key 和私钥内容在摘要及 AI 输入前脱敏。 +- 默认原始日志保留 14 天,失败任务 30 天,Release Evidence 按提交周期冻结;策略可配置。 + +完整日志下载由 `DownloadRepositoryTaskLog` 在后端统一脱敏,Bundle 使用 `.tar.gz` 容器,内含 `context.json`、`result-summary.json`、`task-log.jsonl.zst` 和 `SHA256SUMS`。前端不把 Drawer 可见文本拼成“完整日志”。 + +### 6.4 失败诊断 + +规则诊断仍然是功能成立的基础,不依赖外部模型。输出字段保持: + +```text +Failure Category +Error Code +Failed Step +Likely Cause +Evidence +Recommended Action +``` + +除原有 Sync、Upload 和 DNF 分类外,新增 node offline、stale heartbeat、insufficient space/inode、unsupported filesystem 和 missing tool。 + +错误 Code、中文说明和中文建议来自同一后端错误目录,Task、Node、隔离项及安装结果复用该来源。未知 Code 保留原文和技术 message,并显示统一中文兜底及通用建议,避免不同页面各自翻译而产生冲突。 + +### 6.5 OCAI 智能辅助 + +OCAI 在规则诊断之后工作,不接收无限原始日志。`BuildRepositoryOCAIContext` 输出仓库与任务信息、失败步骤、稳定错误码、容量预检、关键日志窗口、重复统计、RPM/NEVRA/checksum 证据、规则结论和允许建议的操作清单。 + +OCAI 返回问题位置、可能原因、支撑证据和处理建议。用户确认后,Retry、Re-sync、Rebuild Metadata 等操作由 OCManager Repository API 创建任务并执行,OCAI 不直接修改仓库,也不自动触发任务。 + +当前官方 `ocai-service` 定位为只读诊断 Agent,因此本项目不把写操作注册为 AI tool,也不把 `/confirm` 解释成绕过权限的执行授权。 + +## 7. 安全与可靠性设计 + +### 7.1 GPG + +包级策略支持 `DISABLED`、`AUDIT`、`ENFORCE`;旧 `VERIFY_PACKAGE` 作为兼容值与 ENFORCE 等价。DISABLED 不进行签名策略检查;AUDIT 记录签名状态、签名者和检查信息,不因签名策略不满足而阻断入库;ENFORCE 必须使用授权公钥完成校验,未签名、错误签名或未授权包进入 quarantine,不能进入可用源。RPM 格式、损坏、架构和 checksum 校验不因选择 AUDIT 而关闭。 + +Metadata Signing 与包级策略分别配置。节点上报可用 Key 的 `key_ref`、fingerprint、状态和公开公钥信息,仓库仅选择引用。启用时真实生成 `repodata/repomd.xml.asc`,Revision 保存签名状态、Key 引用、指纹、签名路径和公开 Key URL。Generator 依据实际发布能力输出 `gpgcheck`、`gpgkey`、`repo_gpgcheck`;签名材料不完整时拒绝输出假启用配置。切换 Key 只影响新 Revision,不重签或修改历史 immutable Revision。 + +GPG 复现采用两层入口:`repository-gpg.sh` 在临时环境自动生成授权签名、未签名和未授权签名样例,覆盖接收与隔离策略;`repository-gpg-install-demo.sh` 直接读取产品页面生成的 `.repo`,在新的 installroot 中禁用全部默认源,记录公钥 fingerprint、软件包来源仓库、DNF 退出码和安装后的 NEVRA。后者不会另外拼接 baseurl,保证验证的是 Repository Generator、HTTP 发布、GPG 策略和 DNF 安装同一条产品链路。 + +RPM 授权公钥表只保存 `id/repository_id/key_id/fingerprint/public_key_path/created_at`。Metadata Signing 私钥由节点本地受限目录持有,管理面只保存 Key Inventory 和引用。任何私钥不得写入数据库、Git、公开 `.env`、Repository public 目录或 Evidence。 + +GPG 公钥删除与活动内容绑定,而不是与“是否曾经发布过”绑定。活动 Revision 中仍有软件包时拒绝删除公钥;通过按 NEVRA 删除发布空 Revision 后,可以删除公钥,再删除仓库。前端始终提供删除入口并展示该限制,后端以活动 Revision 的实际包数作为最终判定,避免已清空仓库因历史 Revision 永久无法删除。 + +隔离区保存的是未进入 active Revision 的拒收内容,不单独阻断普通仓库删除;活动软件包、未过期上传和运行任务仍受保护。系统用途 `NODE_RPM` 仓库及被安装源引用的仓库另有用途保护,不能套用普通空仓库删除规则。具体删除前置条件由产品 API 统一判断,前端显示其原因。 + +### 7.2 节点安全 + +- 仓库节点内部管理端口只允许管理网访问。 +- 管理服务与节点之间使用 mTLS 或短期签名凭据。 +- worker 使用最小权限账号,不能写入系统其他路径。 +- 节点配置不接受任意 shell 命令和任意绝对路径。 +- `publish_base_url`、`upstream_url` 防止 SSRF,限制协议、网段和重定向。 +- 节点停用后不接受新任务;存在仓库绑定时不能直接删除节点。 + +控制台安装 Node 使用现有标准私有 Repository 保存 `ocmanager-repository-node` RPM,并设 `system_managed=true`、`purpose=NODE_RPM`。它允许上传、RPM/GPG 校验、repodata、immutable 发布和 HTTP 读取,禁止外部上游和普通同步。按项目/架构配置正式 Install Source;Artifact 必须引用其中已发布的合法包、Revision 和 SHA-256,旧 `LEGACY_LOCAL` 不参与默认选择。架构统一规范为 `x86_64/aarch64`,默认“最新”按 RPM EVR 比较,支持选择历史版本。 + +TMS 只下发小型脚本、临时源配置、指定 NEVRA 和 Enrollment 信息;目标 Agent 从固定 Revision 的标准源执行 DNF 安装,依赖可由既有 OC9 源满足。临时源沿用真实 GPG 策略,不使用 `--nogpgcheck`,成功或失败后清理,重试不累积 repo 文件。 + +注册白名单以 `(project_id, tms_agent_id)` 为强身份,架构可作强约束,IP/hostname 仅为附加约束。默认拒绝;创建 Enrollment 和消费 Bootstrap Token 时均检查白名单。授权事务锁定 Enrollment、Allowlist、Token 和 Task,验证后消费 Token 并持久化 `authorization_id`、CSR 摘要与身份快照;事务提交是授权线性化点。证书签发在事务外执行,之后保存到唯一 `repository_node_certificates`,避免长事务持有外部 I/O。 + +普通 consumed Token Replay 返回稳定拒绝;只有完全匹配已授权请求的 authorization、CSR、项目、Agent、架构才可恢复签发,或返回已保存的同一公开证书,不重新授权。Allowlist Disable 先提交则拒绝授权,授权先提交则本次授权可继续;撤销白名单不自动中断已上线 Node,需要使用正式 Disable Node。节点证书有有效期及续期机制,禁用/删除节点后管理面拒绝其 mTLS 任务与心跳请求。 + +### 7.3 日志与 OCAI 安全 + +- 日志摘要和 OCAI 上下文使用同一版脱敏器。 +- OCAI 可连接隔离网内 OpenAI 兼容模型服务。 +- prompt 不携带私钥、JWT、上传 chunk、公钥正文和完整数据库 payload。 +- 每次分析记录 context digest、版本、用户和 session ID。 +- AI 建议不自动执行;操作继续受 JWT、项目权限、节点状态和 mutation lock 保护。 + +### 7.4 恢复 + +1. **Restart**:重启 API、仓库节点 worker/Nginx、TMS、OCAI,验证 current、日志索引和任务 lease。 +2. **Redeploy**:重建容器,保留 MySQL 与仓库节点 bind data,重新执行 acceptance。 +3. **Destroy/Restore**:在 disposable 环境从 MySQL backup、仓库节点 backup 和 manifest 恢复,再执行完整 acceptance。 + +恢复后必须保持 Repository ID、Storage Node ID、active Revision、manifest digest、package inventory、GPG metadata、日志索引和 Validation Report 一致。 + +## 8. 前端页面设计 + +### 8.1 技术边界 + +继续使用 Vue 3、Vue Router、Pinia、TDesign Vue Next 和现有 Axios/`fetchApi`,不新增 UI/CSS framework、状态管理库或重复 HTTP client。 + +### 8.2 导航结构 + +```text +软件仓库 +├── 仓库管理 +├── 任务总览 +├── 仓库节点 +└── 上游源 +``` + +仓库主列表采用服务器分页平铺表格,以系统/产品和版本筛选并在行内展示组织信息,避免分页后的前端分组截断;任务、节点和上游源提供独立一级入口。 + +### 8.3 页面与新增内容 + +| 页面 | 展示内容 | 主要操作 | +| --- | --- | --- | +| 仓库列表 | 产品/版本、类型、架构、包数、内容大小、最近同步、节点名称、状态和 Revision | 创建、编辑、同步、删除、清单导入/导出、批量配置 | +| 上游源 | OpenCloudOS 内置来源、项目地址模板、版本/架构和默认规则 | 新增、编辑、删除自定义来源 | +| 创建/编辑仓库 | 基本信息、上游源快捷/自定义模式、include/exclude、GPG、主仓库节点 | 选择来源、使用快捷规则或自定义输入、选择节点、提交 | +| 仓库节点列表 | 集群汇总、架构、仓库数/真实占用、磁盘已用/总量、版本、心跳和错误 | 手工注册、安装 Node RPM、配置安装源/白名单、探测、停用、删除 | +| 节点 Drawer | 节点容量/工具、该节点全部 Repository/Source、各仓库 logical/physical 大小、Revision、同步状态、最近任务 | 逐仓同步、查看全部任务、预检 | +| 仓库详情 | 概览、软件包、任务、节点分发、仓库配置五个页签 | 软件包页内上传/隔离、批量删除与安装;仓库配置内 GPG、签名、同步计划与 Repo 配置 | +| 任务总览/详情 | 四态统计、项目/仓库/节点/类型/触发/时间筛选、Target、逐包结果和失败摘要 | 查看 Target、取消、重试、脱敏日志 Bundle、OCAI 分析 | +| 智能分析 Drawer | 有界上下文、加载中的 SSE 内容、原因/证据/建议和失败重试 | 在当前任务内查看分析,实际操作仍由 Repository API 执行 | + +容量展示必须同时给出总量、已用、可用、预计峰值、执行后预计使用率、inode 和预检时间,不能只显示百分比。 + +仓库、任务主表使用紧凑的默认列:系统/版本、类型/架构、包数/内容大小或状态/进度合并展示。齿轮“显示列”支持勾选、应用、取消、恢复默认和全部字段,必需身份及操作列不允许隐藏。只在浏览器保存列 ID 和偏好版本,不保存业务数据或凭据;存储异常不阻断使用。服务器分页、筛选、顶部统计和 API 请求不随显示列改变。 + +两个主表复用 TDesign 的表头与横向滚动条吸附,绑定实际滚动容器;空间允许时固定名称与操作列,避免必须滚到最后一行才能横移。实现位于 `RepositoryColumnSettings.vue`、`table-columns.ts` 和 `useRepositoryTableAffix.ts`,不另建表格框架。 + +软件包安装的目标选择保留 TMS Agent ID 身份,支持 IP/Agent 搜索、在线筛选、刷新和选择计数;ID 始终按字符串提交,离线/异常身份禁选,同 IP 不同 Agent 不合并。分发创建仍是现有 `target_ips` 协议,因此同 IP 多身份或连接状态冲突必须禁选并提示确认 TMS 注册信息,不能靠前端排序猜测目标。分发历史 IP 来自原有 `targets[]`,前两个横向排列,更多目标通过 Popup 查看;均衡列宽不改变分发数据面。 + +常用状态、包来源、任务步骤和详情标签采用中文展示,保留原 Code、完整 ID、校验值与原始日志。未知值不翻译成“成功”,错误原因继续使用既有统一错误目录,不在页面复制错误翻译表。 + +### 8.4 已实现页面截图 + +本节界面截图统一使用 2026-10-24 当前部署的真实页面,包含统一查询区、紧凑标签、节点运维、安装和删除预览及统一任务。已完成任务的截图是当前界面读取既有记录,不表示为拍照重新安装、删除或导入。完整操作按[结构化验收报告](私有化软件仓库结构化验收报告.md)对应章节进行;截图中的地址、名称和测试包只代表实验样本,不是部署默认值。文件、捕获范围和 SHA-256 见[2026-10-24 当前页面截图清单](evidence/repository-documentation-screenshots-20261024.json);旧图片与历史测试结果继续保留,原始证据不由重拍图片替换。 + +**显示列与当前仓库主表** + +![显示列和合并字段](images/repository/repository-current-20261024-columns.png) + +**当前分发列表:真实目标 IP 横排,列宽均衡** + +![分发目标与列宽](images/repository/repository-current-20261024-distribution.png) + +**节点运维工作台:Repository、容量、Revision、同步与最近任务** + +![节点运维与同步](images/repository/repository-current-20261024-node-drawer.png) + +**软件包 DNF 安装预览与已有多目标结果** + +![软件包安装预演](images/repository/repository-current-20261024-package-install-preview.png) + +![目标执行结果](images/repository/repository-current-20261024-package-install-result.png) + +**批量删除依赖预演与单任务逐包结果** + +![批量删除预演](images/repository/repository-current-20261024-batch-delete-preview.png) + +![逐包结果](images/repository/repository-current-20261024-batch-delete-result.png) + +**Inventory Import 预演与统一任务** + +![清单导入预演](images/repository/repository-current-20261024-inventory-import-preview.png) + +![清单导入任务](images/repository/repository-current-20261024-inventory-import-task.png) + + +### 8.5 交互状态 + +- `PARTIAL_SUCCESS` 使用 warning,不显示为普通绿色 Success。 +- 节点容量不足时禁用同步/上传提交,并展示各项缺口。 +- 容量数据超过 60 秒未刷新时标记为 stale,不允许直接使用旧结果启动大任务。 +- 离线或已停用节点不能新建 Repository 或新任务。 +- 原始日志默认折叠,优先展示步骤、错误码、关键证据和建议。 +- “交给 OCAI 分析”仅在存在结构化上下文时可用,并显示将发送的字段范围。 + +Repository 的 OCAI 分析入口位于“仓库详情 → 任务 → 任务详情”,不是所有全局 Task Drawer 都提供该入口。确认上下文后,面板紧接辅助分析入口、位于容量和日志之前,并只在当前 Drawer 内滚动及聚焦标题。入口区分准备、分析中、查看分析;已有分析再次点击仅定位,重试另行触发。`useRepositoryAnalysis.ts` 按 task/analysis/session 与请求代次隔离过期响应,`analysis-focus.ts` 负责局部定位;不修改共享 Chat、SSE 或后台任务系统。 + +上下文默认收起但可以展开,结果来自真实模型,仅给建议。共享 Chat 在用户主动停止或 HTTP 200 后异常 EOF、未发出完成/失败事件时,包装面板仍可能显示“分析中”;本次不把这一未闭合边界写成已修复,也不把正常 SSE 完成测试扩张成全部断流恢复已通过。刷新页面后不承诺自动恢复整个本地分析状态。 + +登录表单显示处理中并抑制重复提交。仅对 IdentifyAccount 的登录失败保留密码错误、锁定/限流等原因,避免误跳资源权限 403 页;其它业务的认证/授权处理和后端安全策略不变。 + +## 9. 部署与 OCAI 设计 + +### 9.1 部署拓扑 + +```text +OCManager 管理节点 + manager frontend/nginx + footstone + MySQL/Redis + TMS control plane + ocai-service + +Repository Storage Node A + repository-worker --node-id=node-a + repository nginx + /opt/ocmanager/repository-data + TMS Agent + +Repository Storage Node B(可选主仓或分发副本) + repository-worker/replica task + repository nginx + repository-data + TMS Agent +``` + +部署脚本支持 `manager`、`repository-node`、`ocai` 和可选 `ops-mate` profile。Repository Node 容器不启动 footstone/MySQL,manager 容器不挂载 RPM 数据目录;小规模环境可在同一宿主机运行两组容器,但数据目录和职责仍保持隔离。 + +### 9.2 OCAI 部署 + +官方仓库: + +- `ocai-service`:FastAPI AI 对话和工具编排,提供 `/api/v2/ocai/*`。 +- `ops-mate/diaglite`:诊断任务网关、catalog 和内置 RAG,可选。 +- `ops-mate/diagagent`:多轮主机诊断,可选。 + +关键配置通过环境变量注入,不写入 Git: + +```text +ENABLE_OCAI_DEPLOY=true +OCAI_JWT_SECRET=<与 OCManager 鉴权约定一致> +OCAI_LLM_DEFAULT_HOST= +OCAI_LLM_DEFAULT_NAME= +OCAI_LLM_DEFAULT_API_KEY= +ENABLE_OPS_MATE=true|false +OCAI_DIAGLITE_BASE_URL= +OCAI_DIAGAGENT_BASE_URL= +``` + +Nginx 代理 `/api/v2/ocai/* -> ocai-service:8000`。健康检查: + +```bash +curl http://127.0.0.1:18000/api/v2/ocai/health +curl http://127.0.0.1:13070/api/v2/ocai/health +``` + +### 9.3 Repository 与 OCAI 的接入 + +1. Repository Task Detail 调用 `BuildRepositoryOCAIContext`。 +2. 前端显示上下文摘要并由用户确认发送。 +3. 调用 `/api/v2/ocai/chat`,携带 context digest 和脱敏内容。 +4. 复用现有 `tool_call/tool_result/user_confirm/tool_confirm` 卡片展示过程。 +5. 用户回到 Task Detail 执行受保护操作。 +6. `AppendRepositoryOCAIFeedback` 关联新 task ID 和执行结果,继续会话。 + +后续如增加 Repository 只读 function-call tool,应通过 OCAI 已有 MCP/工具注册机制,只开放 `DescribeTask/DescribePreflight/DescribeLogSummary`,不开放写 RPC。 + +## 10. 验证与验收设计 + +### 10.1 DNF 可信模型 + +三个用例全部使用产品 Generator 生成的真实 `.repo`,并禁用所有默认源。 + +**Case A:仅同步仓** + +```bash +dnf --disablerepo='*' --enablerepo='' install +``` + +**Case B:仅私有仓** + +```bash +dnf --disablerepo='*' --enablerepo='' install +``` + +**Case C:私有包依赖同步包** + +```text +ocm-sync-dependency 仅存在于 Sync Repository +ocm-private-app Requires: ocm-sync-dependency,仅存在于 Private Repository +``` + +仅开启 sync + private 两个仓库,安装后执行 `rpm -q ocm-private-app` 和 `rpm -q ocm-sync-dependency`,并记录 repo ID、baseurl、NEVRA、source repo、exit code、结果和耗时。 + +### 10.2 仓库节点验收 + +- 管理服务节点无 RPM 数据目录,磁盘占用不随仓库规模增长。 +- Repository Node 注册、心跳、离线、停用和删除保护。 +- 上游同步流量和文件落盘均发生在目标仓库节点。 +- 上传 chunk 经管理面鉴权后落到仓库节点,管理服务不产生临时 RPM。 +- 节点 Nginx GET/HEAD 成功,写方法全部拒绝。 +- 节点重启/redeploy 后 current 和 active Revision 一致。 +- 节点迁移绑定 immutable Revision,校验 manifest 后切换。 + +### 10.3 容量验收 + +按生命周期记录 `du --bytes`、文件数量、剩余空间和 inode。负向覆盖空间不足、使用率达到 90%、inode 不足、容量心跳过期、文件系统能力不足、上游大小未知且无上限。实际峰值与理论值差异超过 5% 时必须解释;未知占用未清理或空间不足仍发布直接判定 FAIL。 + +### 10.4 日志验收 + +- 使用大日志样例验证 worker 内存稳定,不把整段日志写入 MySQL。 +- 重复 10 万次相同错误,DB 只保存聚合计数和代表窗口。 +- 原始日志压缩文件 SHA-256 可验证,下载受 JWT/项目权限保护。 +- token、Authorization、URL 密码和 API key 在摘要、导出、OCAI 输入中均不可见。 +- 日志到期后可清理,索引状态同步更新。 +- 节点离线时摘要仍可查询,原始日志明确返回节点不可达。 + +### 10.5 OCAI 验收 + +1. OCAI health、模型连通和 SSE 对话 PASS。 +2. 同步失败和 DNF dependency missing 摘要可得到包含原因、证据和建议的结果。 +3. 验证输入大小上限和脱敏结果。 +4. 验证 OCAI 不具备 Repository 写权限。 +5. Retry 后的新 task 结果可以关联回原 OCAI session。 +6. OCAI 不可用时,规则诊断和 Repository 主流程仍可使用。 + +### 10.6 自动化入口 + +```text +make -C manager repo-e2e +make -C manager repo-diagnosis +make -C manager repo-log-governance +make -C manager repo-ocai +make -C manager repo-report +make -C manager repo-gpg +make -C manager repo-gpg-install-demo +make -C manager repo-distribution +make -C manager repo-recovery +make -C manager repo-evidence-bundle +make -C manager repo-basic-acceptance +make -C manager repo-acceptance-matrix +make -C manager repo-acceptance +``` + +`repo-acceptance` 执行环境和节点预检、CRUD/Batch、节点同步与上传、quarantine、repodata/HTTP、Case A/B/C、GPG、日志摘要/下载/脱敏、规则诊断/OCAI、分发、导出和三层恢复。任一 MUST 失败,命令退出码非零。 + +### 10.7 Evidence + +Evidence 继续按 9 个对外分类打包。新增: + +```text +repository-node-inventory.json +repository-node-preflight.json +repository-node-capacity.csv +repository-task-log-index.json +repository-task-log.zst +repository-task-log.sha256 +repository-ocai-context.redacted.json +repository-ocai-result.json +repository-ocai-sse.log +``` + +所有 Evidence 生成 `SHA256SUMS` 和 `summary.json`。 + +## 11. 预期效果 + +- 用户在 OCManager 中选择已注册仓库节点创建软件仓库;生产环境使用独立数据盘或独立主机,避免占用管理服务系统盘。 +- 创建、同步和上传前可以看到目标节点容量、inode、工具和预计峰值。 +- 大仓库同步由节点直接访问上游,管理服务只做编排和状态管理。 +- 同步、上传、删包和发布均形成不可变 Revision,可追溯、可恢复。 +- 任务详情先显示关键步骤、错误和建议,需要时再下载原始压缩日志。 +- 规则诊断在无 AI 时仍可用;OCAI 可进一步解释日志并给出处理顺序。 +- `.repo` 由真实仓库节点地址和 GPG 策略生成,可用于隔离 DNF 安装。 +- 固定 Revision 可分发到多个节点,校验 manifest 后原子发布。 + +| 项目 | 目标 | +| --- | --- | +| 管理节点 RPM 持久化 | 0 字节 | +| 容量预检 | 每次 mutation 前执行,结果可追溯 | +| 空间不足误发布 | 0 次 | +| 损坏/未授权 RPM 进入 active | 0 个 | +| Remote Sync 半成品发布 | 0 个 | +| 原始日志写入 MySQL | 0 字节 | +| 日志摘要默认响应 | 不超过 64 KiB | +| OCAI 默认上下文 | 不超过 32 KiB,全部脱敏 | +| DNF Case A/B/C | 3/3 PASS,并记录 source repo | +| 单目标/多目标分发 | 所有选中目标的摘要与状态可独立核对 | +| 恢复后验收 | 全部 MUST PASS | + +## 12. 历史实施时间规划 + +下表保留原比赛提交前的实施安排,用于说明设计演进,不再作为当前待办。之后的产品化验收以当前代码、四份权威文档和 152 项机器清单为准;原时间安排保持不改动。 + +| 时间 | 工作内容 | 交付/门禁 | +| --- | --- | --- | +| 8/20-8/25 | 现有 Repository CRUD、同步、上传、Revision、发布、DNF、GPG、诊断、分发、自动化和恢复 | 基线 31/31 PASS,保留 Evidence | +| 8/28 | V4.0 方案重构、接口/数据模型和节点拓扑冻结 | 本文、架构图、增量清单 | +| 8/29 | 独立仓库节点 POC:worker/Nginx/本地 bind、节点直连上游、管理节点无 RPM | POC PASS 后进入正式改造 | +| 8/30-8/31 | Storage Node 数据模型、注册/心跳/状态、仓库绑定和任务路由 | 后端单测、迁移、proto 隔离生成 | +| 9/1-9/2 | 远程节点分片上传、同步落盘、节点发布地址、Generator 改造 | 上传/同步/HTTP 产品链路 PASS | +| 9/3 | 磁盘/inode/工具/文件系统 Preflight 与保守容量估算 | 正向、空间不足、inode、stale 负向 PASS | +| 9/4-9/5 | 日志流式采集、zstd、摘要/模式聚合、脱敏、下载与保留策略 | 大日志、重复日志、脱敏和清理 PASS | +| 9/6-9/7 | 前端仓库节点、容量预检、任务日志和现有页面联动 | Vue 页面、接口联调、真实截图 | +| 9/8 | 部署 `ocai-service`,完成 health、JWT、模型和 SSE 对话 | OCAI 基础链路 PASS | +| 9/9 | Repository 上下文、智能分析入口、建议与执行反馈关联 | OCAI 正向/降级/脱敏测试 PASS | +| 9/10 | 新拓扑下 GPG、Case A/B/C、Generator、单目标及多目标分发回归 | 核心和 Enhanced 全部联调 | +| 9/11 | restart/redeploy/destroy-restore、容量和大日志压力负向 | 恢复与安全负向 PASS | +| 9/12 | 一键 acceptance、Evidence、页面截图、文档和演示流程 | 候选提交包 | +| 9/13 | 全新 OpenCloudOS 9 环境完整复现,冻结结果 | 全量 Release Acceptance PASS | +| 9/14 | 最终复核、提交和缓冲,不新增功能 | 正式提交 | + +9 月 10 日之后不再引入结构性功能。9 月 13 日如有 MUST 未通过,只修复阻断问题,不降低验收标准。 + +## 13. 验收追踪与当前状态 + +8 月 25 日和 Build130 的基线矩阵作为早期历史证据保留。Build141 在此基础上补充同步过滤容错、上传失败终态和国内镜像快捷源;Build143 随后以当时的上游 `master` 为基线完成隔离合并,并在 OpenCloudOS 9.6 上重新执行恢复、TMS 多目标(当时为两个 Agent)、前后端回归、Case A/B/C、1 GiB 日志、真实 OCAI、GPG 和产品分发,AC-01 至 AC-31 为 `31/31 PASS`。Build154 又对后续候选树完成干净基础部署、后端构建、前端测试与发布检查。两个 Agent 只代表 Build143 的多目标增强证据,当前产品和脚本均接受一个或多个在线 Agent。 + +| Category | 已完成基础 | V4.0 新增验收 | 代表性完整功能证据 | +| --- | --- | --- | --- | +| Repository Management | CRUD、产品版本、Task/Lock | Storage Node CRUD、绑定、心跳、迁移保护 | 历史 Build143 `PASS` | +| Package Ingestion | Sync、RPM/tar upload、quarantine、NEVRA | 数据只落仓库节点、节点预检 | 历史 Build143 `PASS` | +| Repository Publishing | repodata、atomic、HTTP | 节点 Nginx、真实 publish URL、新拓扑恢复 | 历史 Build143 `PASS` | +| DNF Validation | Case A/B/C 基线 PASS | 使用仓库节点 Generator 地址重跑 | 历史 Build143 `PASS` | +| Automation | Make/Shell/Compose/Recovery | 完整 Release Acceptance 与 Evidence 聚合 | 历史 Build143 `PASS` | +| Security | GPG 与 HTTP 只读 | 节点通信、大日志脱敏、OCAI 边界 | 历史 Build143 `PASS` | +| Distribution | immutable 分发基线 PASS | 官方 RPM Revision 从仓库节点分发到目标 Agent,并在重启后复测 | 历史 Build143 `PASS` | +| Diagnosis | 规则诊断与报告 PASS | 海量日志摘要、OCAI 分析和反馈 | 历史 Build143 `PASS` | +| Repo Generator | current/revision/GPG 配置 PASS | 使用绑定节点真实 baseurl | 历史 Build143 `PASS` | + +内部 AC-01 至 AC-31 继续作为核心矩阵保留。Build143/Build154 是早期完整功能和干净基础部署记录;当时最近一次完整候选结果为 2026-10-14 的 152 项闭环,见[结构化验收报告](私有化软件仓库结构化验收报告.md#11-当前版本结构化验收闭环)。2026-10-22 前端增量执行 105 项测试、定向质量检查和真实页面验证,未重新生成全量 152 项结果。历史截图和实验数据只用于说明对应版本,不作为当前固定地址、节点数量或部署前置条件。 + +## 14. 风险与控制措施 + +| 风险 | 影响 | 控制措施 | +| --- | --- | --- | +| 独立仓库节点需要新的内部数据通道 | 上传和任务路由改动较大 | 先完成节点 POC;复用现有 chunk session 和 worker,不依赖外部 Agent 改造 | +| 上游无法准确估算大小 | 容量预检可能偏小 | 解析 primary metadata;未知时要求 max_import_bytes;保留 20% margin | +| 管理 API 通用 debug 日志打印 chunk | 日志爆炸或泄露 | Repository upload/GPG 方法只记录元数据,禁止序列化 bytes/secret | +| 仓库节点离线 | 仓库发布和原始日志暂不可达 | 页面显示状态;恢复节点后重新校验,摘要仍保存在 DB;已分发副本是否提供访问由其部署配置决定,不承诺自动故障切换 | +| OCAI 模型地址或 key 不可用 | AI 分析失败 | 规则诊断独立可用;OCAI 失败不阻断仓库主流程,但阻断最终 Enhanced 验收 | +| OCAI 输出不稳定 | 建议不可信 | 提供确定性证据;限制输入;AI 只读;测试事实一致性和降级 | +| TMS/Agent 目标节点不稳定 | 分发验收延迟 | 至少保持一个目标在线;多目标逐节点记录结果;任务绑定 immutable revision;失败可重试 | +| 新 proto 生成产生无关 diff | 扩大变更范围 | 在临时副本按既有 Makefile 生成;无关变化立即停止 | +| 新拓扑破坏旧数据 | 恢复失败 | 只在 disposable 节点做迁移 POC;保留 manifest、DB 和 bind backup | + +以下情况立即标记 BLOCKED,不猜测: + +- 仓库节点必须修改未知外部 Agent 仓库才能工作。 +- 当前 TMS/Agent、OCAI 或目标模型能力与文档不一致。 +- 节点文件系统不支持同文件系统 rename 或相对 symlink。 +- proto 生成导致大范围无关文件变化。 +- 必须大规模重构 footstone 或破坏现有 deploy/compose。 +- 工作区出现来源不明的重要修改,无法安全合并。 + +## 15. 最终发布门禁 + +只有以下全部满足,项目才可声明 `FINAL READY`: + +- 原 AC-01 至 AC-30 在独立仓库节点拓扑上全部 PASS。 +- AC-31 `Release Acceptance` 重新执行并 PASS。 +- Repository Storage Node 可管理、可探测,控制台安装关联正式 TMS Agent。 +- 管理服务节点不持久化 RPM、repodata、quarantine 和原始日志。 +- 同步、上传、发布和 DNF 均使用仓库节点真实路径和地址。 +- 磁盘、inode、工具、文件系统和上游大小预检完整通过。 +- 大日志摘要、压缩、脱敏、下载、保留和清理通过。 +- OCAI 部署、SSE、日志分析、降级和反馈关联通过。 +- GPG、损坏包、危险归档、HTTP 写方法和空间不足负向通过。 +- Case A/B/C、至少一个在线 Agent 的分发和 `.repo` Generator 通过;存在多个 Agent 时补充多目标汇总验证。 +- restart、redeploy、destroy/restore 后完整 acceptance 通过。 +- 新增页面使用真实接口,必要页面截图和 Evidence 已更新。 +- 在干净 OpenCloudOS 9 环境按当前部署文档重复执行。 +- 固定清单 `manager/acceptance/repository-acceptance-criteria.json` 的 152 个唯一 ID 全部有 implementation、tests、evidence 指向;无缺失、重复、意外或非 PASS 项。 +- `make -C manager repo-acceptance` 退出码为 0、`failed_count=0`,文档/图片/链接检查通过,验收后核心代码未发生未经验证的变化。 + +任一项未完成,项目不得标记 `FINAL READY`。 + +## 16. 现场演示流程(约 10 分钟) + +1. 登录 OCManager,进入“仓库节点”,展示节点在线状态、磁盘、版本、工具及其 Repository 列表。 +2. 对目标节点执行 Preflight,展示总容量、可用空间、预计仓库大小和发布峰值。 +3. 创建 OpenCloudOS 9.6 Base Repository 和 Private Repository,并绑定仓库节点。 +4. 触发 Base 同步,展示任务进度、节点磁盘变化、repodata 和真实发布地址。 +5. 上传私有 RPM,展示 NEVRA、来源、Revision 和 quarantine。 +6. 生成 `.repo`,在干净 OC9 中禁用默认源执行 Case C。 +7. 上传损坏或错误签名 RPM,展示 active Revision 不变、规则诊断和关键日志。 +8. 点击“交给 OCAI 分析”,展示原因解释、证据和建议;用户确认后点击“由 OCManager 重试”,由 Repository API 创建任务并回传结果。 +9. 将 immutable Revision 从仓库节点分发到独立 Agent 节点,展示 manifest 和目标结果;现场有多个目标时可同时选择。 +10. 展示预先生成的 `repo-acceptance` 9 类 PASS、SHA-256 Evidence,并回到仓库概览总结同步、上传、发布、诊断和分发结果;完整恢复测试不纳入 10 分钟现场操作。 + +## 17. 参考与图片说明 + +- 历史架构基线:[`architecture/private-repository-plan.md`](architecture/private-repository-plan.md)(当前行为以本方案及实际协议为准) +- 部署说明:[`私有化软件仓库部署说明.md`](私有化软件仓库部署说明.md) +- 验收与演示:[`私有化软件仓库测试验收.md`](私有化软件仓库测试验收.md) +- 结构化验收报告:[`私有化软件仓库结构化验收报告.md`](私有化软件仓库结构化验收报告.md) +- OCAI 部署:[`OCAI-DEPLOY.md`](OCAI-DEPLOY.md) +- Repository 协议:[`../manager/backend/protocol/repository/repository.proto`](../manager/backend/protocol/repository/repository.proto) +- 前端接口:[`../manager/frontend/src/service/repository.ts`](../manager/frontend/src/service/repository.ts) +- OCAI 官方仓库: + +| 图片 | 内容 | +| --- | --- | +| `repo-architecture.png` / `.svg` | 管理面、仓库节点、TMS/Agent 和 OCAI 职责与数据流 | +| `repo-feature-map.png` / `.svg` | 六条前端入口、真实 RPC、后端模块和结果映射 | +| `repo-er-model.png` / `.svg` | 内容、节点、任务、上传、安装授权及签名领域关系 | +| `repo-task-state.png` / `.svg` | Task/Target/逐项结果、取消、重试和并发保护 | +| `repo-publish-flow.png` / `.svg` | 校验、元数据签名、两阶段提交和原子发布 | +| `repository-current-20261024-node-drawer.png` | 当前节点运维视角与同步结果 | +| `repository-current-20261024-package-install-*.png` | 当前安装预览入口与已有多目标任务结果 | +| `repository-current-20261024-batch-delete-*.png` | 当前批量删除预览与已有单任务逐包结果 | +| `repository-current-20261024-inventory-import-*.png` | 当前清单导入预览和已有全局任务 | + +架构配图由 `scripts/render-repository-diagrams.mjs` 统一生成,提供同名 `.drawio` 可编辑导出、SVG 和 PNG。正文优先使用 PNG 便于邮件、Markdown 转 PDF 等场景显示,SVG 用于放大查看;维护入口及导出命令见[配图说明](images/repository/README.md)。 + +## 18. 产品化闭环与验收关系 + +V3 没有重写 Repository Worker、Task、Revision、TMS 或 Atomic Publish 主链,而是在已有能力上补齐长期运维所需的查询、幂等、安全和操作入口。Repository 主列表继续以服务端分页为准;任务、节点、软件包和上游源均使用真实后端数据,不在 Vue 中维护第二份核心统计状态。 + +### 18.1 节点运维视角 + +仓库节点 Drawer 同时展示节点文件系统容量和其承载的 Repository。两类容量严格区分: + +- “当前内容大小”来自 active immutable Revision 中 RPM 的逻辑大小; +- “节点实际占用”来自节点文件系统观测,包含 revisions、staging、quarantine、metadata 和 trash; +- 节点总量、已用、可用和 inode 通过节点心跳及预检上报,不能由 Repository 逻辑大小相加替代。 + +Repository 行可直接触发既有 `SyncRepository`,仍按“预检 → Task → Mutation Lock → Repository Worker → immutable Revision → atomic publish”执行。已有同步任务时按钮进入“同步中”,未配置上游、节点离线或 Repository 禁用时不允许触发。最近任务直接复用全局 Task 查询,并以 `storage_node_id` 作为筛选条件。 + +![节点视角的 Repository、Revision 和同步任务](images/repository/repository-current-20261024-node-drawer.png) + +### 18.2 任务和流程模型 + +一个 `repository_tasks` 记录一次完整业务操作;面向多个 Agent 的执行结果写入 `repository_task_targets`,失败 Target 优先显示。该模型用于软件包安装和节点安装,避免把一次批量操作拆成互不关联的多个任务。任务日志由后端生成统一脱敏 Bundle,错误展示由唯一错误目录提供英文 Code、中文说明和中文处理建议,原始技术信息仍保留在详情与日志中。 + +多文件上传为每个文件建立独立 Upload Session,并使用带唯一约束的 indexed chunk 作为事实来源。网络中断或人工暂停后只上传 missing chunks;页面刷新后允许用户重新选择同一文件,通过 fingerprint 匹配既有 Session,不要求浏览器绕过安全模型自动恢复本地 `File` 对象。 + +批量删包在一次 Preview/Confirm 中绑定 immutable Revision 和 `preview_digest`,执行时只创建一个 `DELETE_PACKAGE` Task、一个新 Revision,并在 `repository_task_result_items` 保存逐包结果。依赖分析禁用目标环境默认源,只计算绑定 Revision 内的直接和传递反向依赖闭包。 + +![当前软件包安装预览,未确认执行](images/repository/repository-current-20261024-package-install-preview.png) + +![多目标安装结果](images/repository/repository-current-20261024-package-install-result.png) + +![单任务批量删除预演](images/repository/repository-current-20261024-batch-delete-preview.png) + +![单任务逐包删除结果](images/repository/repository-current-20261024-batch-delete-result.png) + +### 18.3 Node RPM 产品闭环 + +Node RPM 不使用独立文件分发协议,而是进入受保护的普通私有 Repository:上传后完成 RPM/GPG 校验、生成 repodata、发布 immutable Revision,再由 TMS 下发临时 Repository 配置和指定 NEVRA,目标 Agent 通过 DNF 安装并自动注册。 + +系统用途通过 `system_managed=true`、`purpose=NODE_RPM` 标识。此类仓库禁止配置外部 upstream、禁止普通同步和关键字段误改;被安装源引用时不能删除。默认 Artifact 必须同时满足项目、架构、启用安装源、系统用途、PUBLISHED 和 immutable URL 可访问等条件,并按 RPM EVR 规则选择版本,不能按上传时间判断“最新”。 + +安装前由 `(project_id, tms_agent_id)` 白名单授权。创建 Enrollment 与消费 Bootstrap Token 时均再次检查项目、Agent、架构、状态和有效期;Token 只保存摘要且一次性消费。精确恢复只依赖已持久化的 `authorization_id + csr_sha256 + identity snapshot`,普通 Token Replay 稳定拒绝。节点私钥始终在节点本地生成,Manager 仅保存公开证书材料和证书元数据。 + +### 18.4 Inventory 与机器验收 + +Inventory v2 导出的是 Repository 定义和逻辑引用,不包含 RPM 内容、历史 Revision、私钥、Bootstrap Token、TMS/OCAI 凭据或数据库密码。Import 使用统一 Preview/Confirm 语义,并创建可在全局任务页查询的 `INVENTORY_IMPORT` Task;领域 Job 只保存导入专用的对象映射与结果。 + +![Inventory Import 预演](images/repository/repository-current-20261024-inventory-import-preview.png) + +![Inventory Import 全局任务](images/repository/repository-current-20261024-inventory-import-task.png) + +最终 152 项预期集合固定在 `manager/acceptance/repository-acceptance-criteria.json`,Markdown 不参与推导 expected IDs。机器结果逐项保存 `implementation`、`tests` 和 `evidence`,并检查数量、唯一性、缺失项、意外项和非 PASS 项。只有 P14 文档收口、核心源码 SHA-256 未变化且严格门禁退出码为 0 后,才允许声明最终完成。 + +## 19. 审查修复候选的实现边界 + +本节记录 2026-10-23 开始的审查修复及实现边界,不改变 2026-10-14 的历史验收记录。修复复用现有任务、仓库和分发框架,没有增加数据库迁移或 Proto 变更。2026-10-24 冻结候选完成 152 项验收;2026-10-25 该候选以 `ddbdfea457644ed7d01e6c209e9dfd2168cd6953` 同步到 Gitee `master` 与原 PR #420,PR 当前等待上游评审。 + +- 安装 Preview 先在本次临时 `reposdir/cachedir/persistdir/logdir` 中,对绑定的 immutable Repository 执行受限 `dnf -y makecache`,再使用同一状态执行 `--assumeno install`。允许初始化临时元数据信任,不安装软件包、不向宿主 RPM 公钥库执行手动导入;Confirm 仍为用户确认后的实际安装。缺失或错误的元数据公钥必须失败,不使用 `--nogpgcheck`。 +- Retry 在原任务行锁内复用相同直接父任务尚未结束的子任务,避免多次点击创建重复在途 Retry;历史终态保留,Mutation Lock 继续限制同仓变更。这不是对整条 Retry 树做无限期去重。 +- 上传对象继续按 Upload ID 隔离。缺失、不可读与长度/摘要不匹配分别报告稳定 Code。仅合法持久化 Retry 才能在原结果导入事务中将 FAILED Session 恢复为 READY;须核验原任务、所有者、项目、仓库、节点、冻结对象与有效 lease/fencing,普通 FAILED Session 的成功结果不放行,重复结果仍走已有幂等分支。 +- 控制面每 10 秒独立维护过期任务和离线节点,单轮数据库 Context 最长 5 秒,不依赖节点心跳或慢 TMS 查询。Worker 首次心跳失败后等待恢复,成功前不领取任务;失去执行权后不得发布,Prepare/Confirm 等变更 RPC 不增加无条件自动重放。 +- Node List/Detail 在查询、筛选和完整集合统计前刷新陈旧在线状态。既有 30 秒阈值不变,DISABLED 优先,UNKNOWN 不伪造在线;保留最近容量观测,合法心跳可以恢复 ONLINE。 +- 错误说明仍由唯一后端目录提供,补齐 `GPG_KEY_SYNC_REQUIRED`、`UPLOAD_OBJECT_MISSING`、`UPLOAD_OBJECT_UNREADABLE` 和 `UPLOAD_OBJECT_MISMATCH`。原始技术错误不丢弃,也不在不同 Vue 页面新增翻译表。 +- sysdiagnose 调用 TMS 复用既有 HMAC 规范,重试使用新 nonce。Footstone、sysdiagnose 与 TMS 接收方必须保持 Secret 一致,不能只切接收方 STRICT 而遗漏调用方。 + +相邻模块仅局部修复:命令助手按当前详情 ID 回填参数与目标,切换页签后抽屉保持可见,重跑不自动执行;整机监控从容量 KPI、曲线与告警统一排除已识别光盘,保留原始指标和真正满盘告警。专项结果及范围见[结构化验收报告 11.5](私有化软件仓库结构化验收报告.md#115-独立修复候选专项验证)。 + +## 20. Repository 界面风格统一 + +Repository 列表、任务、节点、上游源和详情软件包查询区复用全站 `SearchBox`,通过 Repository 局部 `RepositoryFilterBar` 统一浅灰背景、前置字段标签、控件高度、查询/清空位置和响应式换行。不修改全局 SearchBox、TDesign 主题或其他业务页面。软件包安装、批量删除保留为独立操作栏,不与查询条件挤在一行。 + +节点运维和任务 Drawer 使用一致的字段列宽及间距,短状态标签按内容收缩,长 ID/地址可换行。固定列表头局部恢复 sticky 定位,避免公共表头分隔线样式覆盖定位后与数据列错位;保留列设置和吸附横向滚动条。SHA-256 采用单行短摘要,Tooltip 保留完整值。 + +此次仅新增局部展示组件,不新增 API、Migration、任务类型或第二套数据来源。2026-10-23 已在现有测试环境替换候选前端、Footstone 和 Worker,完成真实主链路回归;2026-10-25 最新候选已更新到 Gitee `master` 和原 PR #420,PR 等待上游评审。实现与证据见[结构化报告 11.6](私有化软件仓库结构化验收报告.md#116-界面统一与候选产物上线回归),历史完整验收和本次增量分别记录。 + +### 20.1 运行态取消日志一致性 + +2026-10-24 的故障实验发现,取消 DNF 命令时进程可能返回 `signal: killed`,未包装 `context.Canceled`,日志层因此错误记录 FAILED,而 Worker 按真实取消信号上报 CANCELED。管理面正确拒绝不一致日志,随后仅能由租约回收收尾。局部修正为执行 Context 已取消时统一保留取消语义,日志按 CANCELED 落盘;不放松管理面的日志状态、租约或 fencing 校验。针对底层返回进程错误及取消时返回 nil 两个分支补测试。真实恢复结果见[结构化报告 11.7](私有化软件仓库结构化验收报告.md#117-运行态故障与恢复实验)。 + +### 20.2 授权恢复与多文件队列补强 + +已消费 Bootstrap Token 不再产生新授权。相同持久化 authorization、CSR 和项目/Agent/架构身份的精确请求,可继续原签发或返回已保存的同一公开证书;白名单在授权事务提交后的撤销、安装 Task 已结束不追溯取消原授权。首次授权仍在事务中校验有效白名单和 Token;节点禁用及不同请求身份继续拒绝。证书仍只由现有证书表保存,未新增平行模型。 + +上传队列按 Repository ID 隔离组件实例,避免路由复用把其他仓库的 Session/完成状态带入当前仓库。单文件完成只刷新相关数据,不关闭整个队列。离开页面暂停待上传项,保存原 Session 和已传 Chunk;刷新后允许要求重新选择同一文件,仅补缺失片。实际修复和验收边界见[测试验收第 15 节](私有化软件仓库测试验收.md#15-当前候选补验记录)。 + +Node RPM 版本下拉只展示当前项目、目标架构且来自 ENABLED Install Source 的 PUBLISHED Repository-backed Artifact,并核对实际 Repository 的启用、私有上传型和架构属性;切换架构或安装源后失效的已选版本回到“最新可用版本”。历史 Artifact 保留可查,不冒充当前可安装版本。现有 Repository 展示响应不暴露 `system_managed/purpose` 时不伪造前端判断,这两个保护属性、immutable Revision 可访问性和最终 EVR 选择仍由既有后端 Enrollment/安装服务权威校验,不新增前端版本比较器。 + +### 20.3 系统安装源错误响应与运行认证 + +Node RPM 系统安装源继续复用普通 Repository、Upload、Verification、Revision 和 Atomic Publish,保护拒绝不变。本次只把已有领域拒绝统一接入后端错误目录:安装源远程同步返回 `REPOSITORY_NODE_SYNC_FORBIDDEN`,确认删除系统仓返回 `REPOSITORY_SYSTEM_MANAGED`,不可用或架构不匹配 Artifact 返回 `REPOSITORY_NODE_ARTIFACT_NOT_FOUND`。响应保留英文 Code、中文原因、中文建议和原始技术原因;Vue 不再为这些分支另设翻译表。实际部署 API 负向检查及 7 文件摘要见[错误映射验证](evidence/repository-node-rpm-error-validation-20261024.json)。缺少删除确认的参数错误仍为 `InternalError` 英文提示,不把此次专项写成所有错误分支已中文化。 + +外部 OCAI Key、TMS HMAC、共享 JWT 和 MySQL/Redis/ClickHouse 运行凭据分别完成受控轮换,见[外部轮换摘要](evidence/repository-credential-rotation-20261024.json)和[内部认证轮换摘要](evidence/repository-internal-authentication-rotation-20261024.json)。认证值仅在仓库外受保护运行配置及私有回滚材料中保存,文档和 Evidence 仅记录校验结果及摘要。ZooKeeper 当前部署没有启用认证,本次没有把未使用的密码变量冒充为安全能力,也没有扩展 SASL 架构。以上专项通过不替代当前候选完整发布门禁。 + +### 20.4 当前候选最终验收与范围 + +2026-10-24 在 OpenCloudOS 9 对冻结候选实际执行 `make -C manager repo-acceptance`,退出码为 0;核心/增强矩阵 31/31、Evidence Bundle 9/9、全部 11 个执行步骤及严格 152 项校验均通过。预期、唯一预期、实际、唯一实际均为 152,重复、缺失、意外、非 PASS 和不完整证据条目均为 0。结果见[最终候选摘要](evidence/repository-final-validation-20261024.json)、[152 项机器结果](evidence/repository-acceptance-progress-20261024.json)和[完整发布验收](evidence/repository-release-validation-20261024.json)。 + +```text +152/152 PASS +OVERALL_ACCEPTANCE = PASS +FINAL_READY = YES +``` + +全量运行发现的 GPG 临时密钥用例使用了过长 Unix socket 路径,已只在测试夹具采用受控短临时目录并完整重跑;隔离运行器同步固定工具路径和最小环境注入。此收口没有修改 Repository 业务链路或放松 GPG/mTLS/Mutation Lock。源码清单、锁文件和运行产物指纹与证据绑定,P14 只更新文档与证据链接。 + +标准 Repository、Task/Target、Enrollment、Indexed Chunk、Scheduler 和 Revision/Atomic Publish 设计保持原有架构。统一任务样本、Node RPM 安装源和白名单授权竞态、双架构 RPM/TMS/自动注册、上传恢复及分发均有真实证据;详见[操作链路审计](evidence/repository-action-traceability-20261024.json)。 + +验收范围继续保持明确:ZooKeeper 的历史部署未启用认证,本轮未扩展 SASL;Repository 定向类型检查通过不等于全站历史 TypeScript 问题已清零;旧版本号 RPM 已验证页面选择与 EVR 策略,不声称对运行节点执行过历史代码降级;TMS Agent 需先按既有 TMS 部署方式安装并在线,本轮自动安装的是 Repository Node RPM,不是为新机器自动引导部署 TMS。遗漏删除确认的普通参数错误仍可能返回英文技术提示,不能据此宣称所有参数校验均已中文化。 diff --git "a/docs/\347\247\201\346\234\211\345\214\226\350\275\257\344\273\266\344\273\223\345\272\223\346\265\213\350\257\225\351\252\214\346\224\266.md" "b/docs/\347\247\201\346\234\211\345\214\226\350\275\257\344\273\266\344\273\223\345\272\223\346\265\213\350\257\225\351\252\214\346\224\266.md" new file mode 100644 index 0000000000000000000000000000000000000000..f2574369a5b8872b413f41165e4156fd4597dcfc --- /dev/null +++ "b/docs/\347\247\201\346\234\211\345\214\226\350\275\257\344\273\266\344\273\223\345\272\223\346\265\213\350\257\225\351\252\214\346\224\266.md" @@ -0,0 +1,1102 @@ +# OCManager 私有化软件仓库测试验收 + +| 项目 | 内容 | +| --- | --- | +| 文档版本 | V1.0 | +| 更新日期 | 2026-10-25 | +| 适用读者 | 测试人员、评审人员、部署和运维人员 | +| 验收基线 | OpenCloudOS 9;代码功能基线 `ddbdfea457644ed7d01e6c209e9dfd2168cd6953` | +| 配套文档 | [方案设计](私有化软件仓库方案设计.md) · [部署说明](私有化软件仓库部署说明.md) · [用户手册](私有化软件仓库用户手册.md) · [结构化验收报告](私有化软件仓库结构化验收报告.md) | + +## 文档说明 + +### 验收背景 + +OCManager 私有化软件仓库集中管理发行版上游包和业务私有 RPM,提供同步、上传校验、GPG 策略、repodata/Revision 发布、节点分发、任务诊断和客户端安装配置。验收需要确认软件包来源可信、发布内容完整、节点操作符合权限,并能由 OpenCloudOS 9 客户端通过 DNF/YUM 安装。签名策略、故障恢复、并发和多架构差异都可能影响实际交付,因此需要按完整链路复现并保存可核对证据。 + +### 验收范围与证据口径 + +本文提供自动化验收入口、关键链路复现步骤、通过标准和证据定位方式。测试人员可先运行单项脚本,也可在隔离的 OpenCloudOS 9 验收环境运行完整入口。范围包括前后端质量、数据库迁移、仓库 CRUD、上游同步、上传与恢复、GPG、DNF 安装、Revision 分发、节点安装注册、Inventory、OCAI、安全负向场景和回归检查。 + +当前代码候选在 OpenCloudOS 9 完成 152 项机器验收、31 项核心与增强矩阵及 9 个 Evidence Bundle。结果绑定对应代码和测试环境;Build154、Build143 及 2026-10-14 的实验记录按各自日期和范围保留。复现时确认操作系统、工具版本、在线节点和外部依赖,以本次退出码、结构化结果和日志作为验收依据。 + +### 阅读路线 + +第 1–3 节说明环境、运行入口和证据核对;第 4–6 节保留演示与历史复现记录;第 7 节列出当前产品流程的前提和通过标准。第 12–16 节是后续补充的候选专项、运行态恢复、补验记录和最终门禁。该组章节编号保留,以维持结构化验收报告和既有证据链接的对应关系。 + +命令使用仓库相对路径和 Linux shell。私有 DSN、Token、OCAI 密钥、节点地址及临时认证材料由验收环境注入,不写入 Git 或 Evidence。页面截图来自 2026-10-24 当前产品界面,SHA-256 和场景说明见[当前页面截图清单](evidence/repository-documentation-screenshots-20261024.json)。截图用于识别操作入口,历史试验数据按原始日期记录。 + +## 目录 + +**环境与执行入口** + +- [1. 验收环境](#1-验收环境) +- [2. 执行验收与结果校验](#2-执行验收与结果校验) +- [3. Evidence 核对](#3-evidence-核对) + +**演示流程与历史实验** + +- [4. 10 分钟现场演示](#4-10-分钟现场演示) +- [5. 运行边界](#5-运行边界) +- [6. 提交材料与方式](#6-提交材料与方式) + +**当前产品能力与机器验收** + +- [7. 当前产品化功能与 152 项结果](#7-当前产品化功能与-152-项结果) + +**专项复核与最终门禁** + +- [12. 独立审查修复候选的定向复核](#12-独立审查修复候选的定向复核) +- [13. 界面统一与已部署候选的联验](#13-界面统一与已部署候选的联验) +- [14. 运行态故障与恢复实验](#14-运行态故障与恢复实验) +- [15. 当前候选补验记录](#15-当前候选补验记录) +- [16. 当前候选最终完整验收](#16-当前候选最终完整验收) + +## 1. 验收环境 + +完整验收在 OpenCloudOS 9 控制节点执行。开始前确认: + +- Docker 与 Docker Compose v2 可用,当前用户可以管理容器。 +- `go`、`node/npm`、`jq`、`curl`、`dnf`、`rpmbuild`、`rpm/rpmkeys`、`createrepo_c`、`gpg` 和 `mysql` 可用。默认 socket 模式需要可按需启动的 MariaDB systemd service;独立容器数据库模式见 15.1,不要求操作宿主数据库。 +- `config/.env` 已按现有部署配置,OCManager 与 TMS 由当前仓库的 deploy/compose 链路启动。 +- 仓库功能验收至少需要一个在线 Repository Storage Node;分发验收至少需要一个在线 TMS Agent。各角色可以按资源和安全要求分机部署或合并部署,脚本不按机器角色排除已注册且在线的 Agent。 +- 宿主机 bind 目录、测试端口和磁盘空间满足 preflight。 +- 当前没有进行中的 TMS 分发任务。验收会在空闲边界重启 TMS 控制面。 +- 默认 socket 模式下,非 root 用户先在当前终端执行一次 `sudo -v`;独立容器数据库模式只管理自己的测试数据库,其他需要 sudo 的工具步骤仍按各脚本预检执行。不要把 sudo 密码写入命令脚本或 Evidence。 + +### 1.1 工具与版本边界 + +| 组件 | 支持基线 | 历史 Build130 记录 | 说明 | +| --- | --- | --- | --- | +| 操作系统 | OpenCloudOS 9 | OpenCloudOS 9.6 | Worker 固定使用官方 OC9.6 镜像 | +| Docker / Compose | Docker 20.10+ / Compose v2 | Docker 28.0.1 / Compose v2 | 不支持旧的 `docker-compose` v1 语义 | +| Go | 1.25+ | 1.26.3 | 与 `manager/backend/go.mod` 一致 | +| Node.js | 20+ | Node 20 容器 | 不使用 Node 18 或更旧版本 | +| MariaDB / MySQL | MariaDB 10.11 或 MySQL 8.0 兼容语法 | MariaDB 10.11.18 | 迁移脚本按文件名顺序执行 | +| RPM 工具链 | OC9 自带 `rpm/rpmkeys/rpmbuild` | rpm 4.18 系列 | 不混用其他发行版的 RPM 结果冒充 OC9 Evidence | +| 仓库工具 | `dnf-plugins-core` + `createrepo_c` | OC9 官方包 | `reposync` 和 `createrepo_c` 缺失时 preflight 直接失败 | +| GPG | `gnupg2` + `rpm-sign` | GnuPG 2.4 / OC9 rpm-sign | 管理面只保存公钥信息;Metadata Signing 私钥仅保存在 Node 的受限目录 | + +`config/env.example` 使用公开 Registry 作为默认值。隔离网必须先将同版镜像导入内网 Registry,再通过 `OCM_IMAGE_MIRROR`、`GO_IMAGE`、`RUNTIME_IMAGE`、`NODE_IMAGE`、`NGINX_IMAGE` 和 `REPOSITORY_RUNTIME_IMAGE` 覆盖,不要直接修改 Dockerfile。 + +### 1.2 后端测试边界 + +未带 `integration` Build Tag 的 Repository 单元/受控夹具测试可先在源码快照执行;它们不能代替真实数据库或跨主机回归: + +```bash +cd manager/backend +go test \ + ./services/footstone/application \ + ./services/footstone/domain/repository/po \ + ./services/footstone/domain/repository/repositoryairepo \ + ./services/footstone/domain/repository/repositorycontentrepo \ + ./services/footstone/domain/repository/repositorydistributionrepo \ + ./services/footstone/domain/repository/repositorylockrepo \ + ./services/footstone/domain/repository/repositoryrepo \ + ./services/footstone/domain/repository/repositorystoragenoderepo \ + ./services/footstone/domain/repository/repositorytaskrepo \ + ./services/footstone/domain/repository/repositoryupstreamprofilerepo \ + ./services/footstone/domain/repositoryAIService \ + ./services/footstone/domain/repositoryContentService/... \ + ./services/footstone/domain/repositoryDiagnosisService \ + ./services/footstone/domain/repositoryDistributionService \ + ./services/footstone/domain/repositoryInventoryService \ + ./services/footstone/domain/repositoryNodeService \ + ./services/footstone/domain/repositoryPackageInstallService \ + ./services/footstone/domain/repositoryScheduleService \ + ./services/footstone/domain/repositoryService \ + ./services/footstone/domain/repositoryTaskService \ + ./services/footstone/domain/repositoryUpstreamService \ + ./services/footstone/infrastructure/repository/... \ + ./services/footstone/repositoryworker/... +``` + +原有 `instancerepo` 查询与 Repository 入口受影响时,可用下面的聚焦命令验证相关查询,不会启动依赖数据库配置的旧集成用例: + +```bash +cd manager/backend +go test ./services/footstone/domain/repository/instancerepo \ + -run 'TestList(OSNamesDeprecatedCountMatchesFilteredDistinctNamesBeforePagination|Instances)' +``` + +`go test ./...` 还会运行上游已有的 Kafka、Redis、配置加载等集成测试。该命令必须在 `config/.env` 已配置、Footstone 测试配置可读取、Kafka 与 Redis 等依赖已启动后执行。Repository/Presentation 相关集成测试必须在受控 OC9 测试环境准备有效 `bin/conf/application.yaml` 和实际依赖后运行,不能仅以缺少配置长期跳过并仍声称完整回归 PASS;配置含 Secret 时放在临时运行目录并限制权限。完全无关的上游集成前提单独记录,不与 Repository 专项结果混为一项。 + +当前产品化功能还需覆盖 Node Enrollment/Allowlist/证书恢复、Scheduler、Indexed Chunk、Task Target、GPG/Import 与 Preview 的新增测试包。每次以实际修改包清单执行测试和 `go vet`,并单独保存 `go build ./...` 结果,不能用上述基础命令列表代替所有新包。 + +真实数据库集成测试需要 `-tags=integration` 和专用测试数据库的 `OCM_REPOSITORY_TEST_DSN`;不设置 DSN 的部分用例会 `SKIP`,所以仅看到 `go test` 退出 0 不能认定它们通过。DSN 使用 Go MySQL driver 格式,指向可丢弃数据库并开启 `parseTime=true`;测试会建表和清理夹具,禁止指向生产库。在 `manager/backend` 中交互读取私有 DSN 后运行: + +```bash +read -r -s -p 'Disposable test database DSN: ' OCM_REPOSITORY_TEST_DSN +printf '\n' +export OCM_REPOSITORY_TEST_DSN +go test -tags=integration -count=1 -v \ + ./services/footstone/domain/repositoryService \ + ./services/footstone/domain/repositoryTaskService \ + ./services/footstone/domain/repositoryScheduleService \ + ./services/footstone/domain/repositoryContentService \ + ./services/footstone/application +unset OCM_REPOSITORY_TEST_DSN +``` + +跨主机 Presentation 用例另需 `OCM_CROSS_NODE_*` 的服务证书、客户端 CA、预期 Fingerprint 和上传摘要参数,字段见 `presentation/repository_node_cross_host_integration_test.go`。先准备实际可达的 mTLS Worker 和临时运行配置,再执行对应测试;记录 PASS/FAIL/SKIP 数,不把缺参数的 SKIP 汇总成 PASS。使用 `AutoMigrate` 的测试只证明服务模型可用,迁移 SQL 的空库、v17→v18 和重入由独立数据库验证完成。 + +前端质量入口在 `manager/frontend` 执行: + +```bash +npm ci +ESLINT_USE_FLAT_CONFIG=false npx eslint \ + src/pages/repository src/service/repository.ts src/service/repository.test.ts \ + src/router/modules/base.ts src/router/modules/base.repository.test.ts \ + src/pages/login/components/components-login.vue \ + src/pages/login/components/components-login.test.ts \ + src/utils/http.ts src/utils/http.test.ts scripts/typecheck-repository.mjs \ + --ext .vue,.ts,.mjs --max-warnings 0 +npm run typecheck:repository +npm run test:repository +npx stylelint 'src/pages/repository/**/*.vue' src/pages/login/components/components-login.vue +npm run build +``` + +`typecheck:repository` 使用锁定的 `vue-tsc` 对 Repository 相关诊断执行门禁,不等于全前端历史问题均已消除;`npm run typecheck` 是全前端检查。记录两者实际范围与退出码,不能把 Vite 构建成功当作 TypeScript 检查成功。正式页面还需在 1440×900 与 1920×1080 检查 Drawer、Dialog、选择器、任务与 OCAI 状态。 + +`test:repository` 以 `vitest.repository.config.mts` 的明确 include 清单为准,包含 Repository、相关 HTTP 登录分支与登录表单测试,不额外启动 development mock watcher。上面 Stylelint 是本次受影响范围,`npm run stylelint` 是全项目额外检查,不能混淆两者。本轮未升级 Node、Vue、TypeScript、TDesign 或锁文件中的依赖。 + +### 1.3 分发目标 Agent 前置检查 + +分发目标不是在“仓库节点”页面创建的数据。源 Repository Node 由仓库的 `primary_storage_node_id` 决定,目标节点由 TMS asset-manager 注册并由 heartbeat 判活。新节点必须安装 `ocm-agent`、连接 TMS Gateway,随后才会出现在 `ListRepositoryDistributionNodes` 返回中。 + +PoC 可以显式使用 `auto_register: true`,Agent 首次连接后自动登记;生产默认使用 `auto_register: false`,需先调用 `/api/v1/assets/add`。无论哪种模式,验收前均执行: + +生产默认启用 TMS 严格 HMAC,直接伪造 `X-User-Name` 会被拒绝。查询时使用仓库提供的签名封装: + +```bash +bash tms/deploy/scripts/gw-sign.sh -X POST \ + -H 'Content-Type: application/json' \ + -d '{}' \ + http://127.0.0.1:8080/api/v1/assets/query | + jq '.data.items[] | {ip, agent_id, status}' +``` + +安装 Agent 前先运行 `scripts/configure-agent-package.sh <安装包目录> `,不要依赖外部 Agent 打包脚本是否能替换某个固定默认地址。systemd 未提供 `HOME` 时,分发脚本会从运行账号信息推导目录;也可在 Agent 服务中显式设置 `OCM_REPOSITORY_MIRROR_ROOT`。该变量必须是当前节点上的绝对数据目录,但源码和文档不内置任何实验账号或主机路径。 + +验收记录目标 IP、Agent ID 和在线状态。目标必须与源仓库节点所在机器不同,且必须为在线状态;仅注册但心跳离线的节点不得作为 PASS Evidence。 + +跨机器独立目标是分发验收的 Evidence 要求,不是前端隐藏同机在线 Agent 的产品规则。安装以字符串 Agent ID 精确选择;分发创建目前按 IP 选择,同 IP 多身份/状态冲突会禁选并说明原因。该负向保护应验证,不能删除未知历史记录或伪造节点来获得可点按钮。 + +Repository 数据默认使用宿主机 bind mount: + +```text +${REPOSITORY_DATA_HOST_DIR:-/opt/ocmanager/repository-data} +-> /var/lib/ocmanager/repository-data +``` + +独立 Node 的 Worker/上传数据接口使用 `rw`,Publisher 仅以 `ro` 挂载 `public/`;管理面 Footstone 不挂载持久化 RPM。验收脚本只在 disposable 目录创建恢复样例,不删除正式 Repository 数据。 +MariaDB 如在验收前未运行,脚本会临时启动并在结束时恢复初始状态。 + +### 1.4 控制台交互增量复现 + +以下检查在完成部署、拥有项目权限后执行;不要求重新下载发行版镜像,不为界面验证执行实际安装、删包或大源同步。截图中的地址、仓库名和数量是实验数据,现场使用自己的资源。 + +| 检查 | 操作 | 通过标准 | +| --- | --- | --- | +| 紧凑列与偏好 | 仓库/任务页打开“显示列”,勾选后应用,再打开取消修改;刷新后检查 | 应用生效、取消不改变当前列、偏好可保留;恢复默认后需点击应用,必需列始终保留 | +| 表头与横向滚动 | 显示全部字段,使用长表滚动到中段 | 可直接使用视口内横条,表头可见,名称与操作在空间允许时固定,不改变分页/查询结果 | +| 节点选择 | 选包后打开“安装到节点”,按 IP/Agent 搜索、切换在线、刷新 | 计数正确,离线/异常身份禁选,有效选择不因搜索隐藏而丢失,长 Agent ID 不损失精度 | +| 只读安装预览 | 对受控已发布包与在线 Agent 点击“预览安装” | 结构化目标结果可见,宽表不挤走操作按钮;不点确认安装时不能当作实际安装成功 | +| 分发列表 | 打开已有分发记录,查看目标摘要、详情和校验值 | 前两个目标横排,更多目标可展开;历史失败/离线目标不被当前在线列表抹掉,未完成目标不把默认退出码当成功 | +| 分发身份冲突 | 查看存在同 IP 多身份/状态冲突的环境 | 创建选项明确禁选并提示;无此环境时只记录单测覆盖,不能伪造真实冲突实验 | +| OCAI 定位与防重 | 从仓库详情的失败任务打开上下文并确认,返回上方再点“查看分析” | 当前 Drawer 定位分析区域,主页面不强制跳转;查看已有分析不新建会话,过期请求不串入新任务 | +| 中文与原文 | 查看状态、来源、版本、任务步骤、错误详情 | 中文可读,原 Code、完整标识、SHA-256、未知技术值与原始日志仍可追溯 | +| 登录反馈 | 用专用测试账号或单测验证成功、失败与请求中状态 | 防重复提交,后台锁定/限流原因不误跳 403;不能通过错误密码反复锁定真实管理员 | + +仓库内 Task Drawer 的 OCAI 与全局任务摘要 Drawer 不是相同入口,不能在全局抽屉找不到按钮后认定服务未部署。模型调用需真实服务和凭据;仅见面板或单测 PASS 不代表真实模型完成。共享 Chat 主动停止/异常 EOF 的结束事件边界未在本轮修复,可能仍显示“分析中”,需核对服务与 SSE 完成事件,不能将其写成已验证的自动断流恢复。 + +2026-10-22 在 `859648f` 上的增量结果:16 文件、105/105 Vitest,定向 ESLint/Stylelint、Repository vue-tsc 和 Vite build 均通过;其它模块类型诊断及既有 chunk-size 提示保留。真实页面与只读模型观察、安装预览和历史分发展示另见[页面证据清单](evidence/repository-ui-captures-20261022.json)及[结构化报告第 5 章](私有化软件仓库结构化验收报告.md#5-前端设计)。历史章节的 28/29/30/48 项计数不改成 105,它们属于对应旧版本。 + +本节结果不重新生成 `152/152 PASS`,也不证明刚刚完成新安装、新分发、后端回归或双架构部署。最终发布/全量门禁仍执行第 7 节,不通过人工修改 Markdown 把历史结果迁移成当前全量结果。 + +## 2. 执行验收与结果校验 + +当前保留两层验收。`repo-acceptance-matrix` 实际运行既有 AC-01 至 AC-31 核心矩阵;`repo-acceptance` 先运行恢复、核心矩阵和 9 类 Evidence Bundle,再检查当前候选的 152 项结果文件。152 项的机器预期集合固定在 `manager/acceptance/repository-acceptance-criteria.json`,结果文件需来自当前源码的专项、浏览器、双架构、安全与回归证据;校验器不会凭空执行或生成全部 152 个产品用例。 + +在仓库根目录指定源 Repository Node 和至少一个目标 Agent IP。`P12_TMS_TARGET_IPS` 只用于 TMS restart 恢复测试;Distribution 使用独立参数: + +### 2.1 总入口必填参数 + +| 变量 | 格式 | 取值方式 | +| --- | --- | --- | +| `P12_TMS_TARGET_IPS` | `""` 或空格分隔的多个 IP | TMS restart 后需复测的已注册 Agent;至少一个,脚本按实际传入数量验证 | +| `P12_TMS_URL` | `http[s]://host:port` | 恢复 Compose 中 Footstone 使用的 TMS 根地址;默认同机 `http://host.docker.internal:8080` | +| `P12_TMS_API_BASE` | `http[s]://host:port/api/v1` | TMS 恢复脚本直接访问的 API 地址;使用自定义 TMS 时应与 `P12_TMS_URL` 指向同一服务 | +| `OCM_REPOSITORY_DISTRIBUTION_TARGET_IPS` | `""` 或空格分隔的多个 IP | `/api/v1/assets/query` 中已注册且心跳在线的 Agent;支持一个或多个目标 | +| `OCM_REPOSITORY_DISTRIBUTION_SOURCE_NODE_ID` | UUID | “软件仓库 -> 仓库节点”的 Repository Storage Node ID | +| `OCM_REPOSITORY_DISTRIBUTION_REPOSITORY_ID` | UUID | 已发布仓库的 Repository ID | +| `OCM_REPOSITORY_DISTRIBUTION_REVISION_ID` | UUID | 该仓库已发布的 immutable Revision ID,不填 `current` 或 `latest` | +| `OCM_REPOSITORY_API_BASE_URL` | `http[s]://host:port` | 可访问 Footstone Repository API 的管理面地址 | +| `OCM_REPOSITORY_OCAI_BASE_URL` | `http[s]://host:port` | OCManager 已转发 OCAI 接口的地址,通常与管理面一致 | +| `OCM_REPOSITORY_OCAI_MODEL` | 模型名 | OCAI 模型列表中已配置的 OpenAI-compatible 模型 | +| `OCM_REPOSITORY_ADMIN_PASSWORD` | 私有环境变量 | 用于换取 JWT;不写入脚本、`.env.example`、Evidence 或 Git | +| `OCM_REPOSITORY_RELEASE_OUTPUT` | 仓库内被 `.gitignore` 排除的相对路径 | 每轮使用新目录,不覆盖历史 Evidence | +| `OCM_REPOSITORY_ACCEPTANCE_RESULTS` | 本次 `acceptance-results.json` 文件 | 必须包含固定 152 个 ID 及 implementation/tests/evidence,且与本次候选源码和实际测试相符;不能复用旧 PASS 代替新验证 | +| `OCM_REPOSITORY_ACCEPTANCE_VALIDATION_OUTPUT` | 本次结果校验输出目录 | 保存集合/状态/路径检查摘要,不与实时测试结果混淆 | + +如果部署已有 JWT,产品分发脚本也可用 `OCM_REPOSITORY_API_TOKEN` 和 `OCM_REPOSITORY_API_TOKEN_HEADER` 代替管理员密码。它们同样只能作为当前进程的私有变量。 + +```bash +export P12_TMS_TARGET_IPS="" # 多目标时继续追加,以空格分隔 +export P12_TMS_URL="http://:" +export P12_TMS_API_BASE="http://:/api/v1" +export OCM_REPOSITORY_DISTRIBUTION_TARGET_IPS="" +export OCM_REPOSITORY_DISTRIBUTION_SOURCE_NODE_ID="" +export OCM_REPOSITORY_DISTRIBUTION_REPOSITORY_ID="" +export OCM_REPOSITORY_DISTRIBUTION_REVISION_ID="" +export OCM_REPOSITORY_API_BASE_URL="http://127.0.0.1:13071" +export OCM_REPOSITORY_OCAI_BASE_URL="http://127.0.0.1:13071" +export OCM_REPOSITORY_OCAI_MODEL="deepseek-v4-flash" +export OCM_REPOSITORY_ADMIN_PASSWORD="" +export OCM_REPOSITORY_RELEASE_OUTPUT="$PWD/.run/repository-poc/evidence/release-acceptance" +export OCM_REPOSITORY_ACCEPTANCE_RESULTS="<本次152项结果JSON文件>" +export OCM_REPOSITORY_ACCEPTANCE_VALIDATION_OUTPUT="$PWD/.run/repository-acceptance-validation" +sudo -v +make -C manager repo-acceptance +``` + +缺少可用源仓库节点、已发布的真实 Revision、在线目标 Agent、OCAI 实际模型配置或其他 MUST 前置条件时,功能脚本可能输出 `BLOCKED` 并以 3 退出;输入/结果结构错误由校验器以非零退出。统一成功条件是退出码 0,不能仅识别某个失败数字。缺少 152 项结果时不能把核心 `31/31 PASS` 写成完整产品 PASS。凭据只通过私有环境变量提供,不写入脚本或 Evidence。 + +如需单独复核智能辅助分析,可运行: + +```bash +export OCM_REPOSITORY_OCAI_BASE_URL="http://127.0.0.1:13071" +export OCM_REPOSITORY_OCAI_MODEL="deepseek-v4-flash" +export OCM_REPOSITORY_ADMIN_PASSWORD="" +make -C manager repo-ocai +``` + +`repo-ocai` 会自动寻找最近的失败 Repository 任务,也可通过 `OCM_REPOSITORY_OCAI_TASK_ID` 指定任务。它验证 OCAI 健康状态、32 KiB 上下文边界、SHA-256、脱敏、只读提示约束和真实 SSE 完成事件;Evidence 只保存结构化摘要、模型清单与哈希,不保存完整模型回答或认证信息。 + +### 2.2 GPG 的两类复现入口 + +GPG 验收分为自动化回归和真实产品链路两层,两者用途不同: + +```bash +# 自包含回归:临时生成授权/未授权密钥与 RPM,验证正向安装和负向隔离。 +make -C manager repo-gpg + +# 产品链路:读取控制台下载的 .repo,只启用该仓库并真实安装签名 RPM。 +export OCM_REPOSITORY_DEMO_ROOT="${PWD}/.run/repository-demo" +export OCM_REPOSITORY_DEMO_REPO_FILE="${OCM_REPOSITORY_DEMO_ROOT}/repos/oc9-demo-private.repo" +export OCM_REPOSITORY_DEMO_PACKAGE=ocm-gpg-demo +make -C manager repo-gpg-install-demo +``` + +第二个入口不会生成或改写 `.repo`,它直接使用 Repository Detail 页面“Repo 配置”下载的产品输出。脚本逐次创建 +`${OCM_REPOSITORY_DEMO_ROOT}/runs/<时间戳>/`,在新的 `installroot/` 中禁用所有系统默认源,依次验证: + +```text +repomd.xml HEAD 200 +-> 下载并解析授权公钥 fingerprint +-> 在新 installroot 中受限 -y makecache,准备 Metadata 公钥并验证签名 +-> repoquery 证明 ocm-gpg-demo 的 source repo +-> DNF 真实安装且 exit code 为 0 +-> rpm --root 查询已安装 NEVRA +-> summary.json + SHA256SUMS 固化证据 +``` + +任一步失败都会返回非零,并保留本次运行目录。录制前先执行 `sudo -v`,不要把密码写进脚本或命令历史。 + +### 2.3 细粒度复现入口 + +完整验收之外,各模块可独立复现。所有入口都由当前仓库脚本实现,不要求直接操作数据库或仓库数据目录: + +| 入口 | 验证内容 | +| --- | --- | +| `make -C manager repo-e2e` | 构建受控 RPM,执行只启用指定仓库的 Case A/B/C | +| `make -C manager repo-gpg` | 授权签名正向、未签名/未授权签名隔离和节点验签 | +| `make -C manager repo-gpg-install-demo` | 使用产品生成的 `.repo` 在全新 installroot 中真实安装 | +| `make -C manager repo-diagnosis` | 同步、上传和 DNF 失败分类、证据与建议 | +| `make -C manager repo-log-governance` | 大日志流式压缩、64 KiB 摘要、脱敏、索引和清理 | +| `make -C manager repo-ocai` | 脱敏上下文、真实模型 SSE、只读边界和降级 | +| `make -C manager repo-distribution` | immutable Revision 经 TMS/Agent 分发到目标节点 | +| `make -C manager repo-report` | 导出 Repository inventory、validation 和 distribution 报告 | +| `make -C manager repo-recovery` | restart、redeploy、destroy/restore 和任务恢复 | +| `make -C manager repo-evidence-bundle` | 聚合 6 个 Core 与 3 个 Enhanced Evidence Bundle | +| `make -C manager repo-release-check` | 提交前检查私钥、凭据、个人路径、临时产物、脚本语法、JSON 和图片链接 | +| `make -C manager repo-basic-acceptance` | 检查可选组件边界及当前基础部署健康状态,不要求 OCAI、Repository Node 或 Agent | +| `make -C manager repo-acceptance-matrix` | 执行并聚合包含 Core 与 Enhanced 的 AC-01 至 AC-31 矩阵 | +| `make -C manager repo-acceptance-criteria` | 只检查固定 152 个预期 ID 的结构和唯一性,不连接真实产品 | +| `make -C manager repo-acceptance-results-test` | 对结果校验器执行正负向自测,不表示产品用例通过 | +| `bash scripts/repository-acceptance-results.sh --results <本次结果.json> --strict` | 只严格检查已生成的 152 项结果及引用完整性,不重跑产品流程 | +| `make -C manager repo-acceptance` | 实际执行恢复、31 项核心和 Bundle,再严格校验已提供的 152 项结果 | + +脚本默认将 Evidence 写到 `.run/repository-poc/evidence/`。需要归档多轮结果时,通过各脚本说明中的 +`OCM_REPOSITORY_*_OUTPUT` 环境变量指定新的版本化目录,避免覆盖上一轮证据。 + +正式部署脚本的数据库升级回归可单独执行: + +```bash +bash scripts/tests/mysql_metadata_upgrade_test.sh +``` + +该测试不连接真实数据库,而是验证 `deploy.sh` 的自然版本发现顺序、真实鉴权就绪门禁、密码传递边界和失败传播。当前迁移目录已到 v18。实际 SQL 兼容性检查使用 `scripts/tests/mysql_migration_compatibility_test.sh`,它会创建并清理临时 Docker 数据库,需要 Docker、对应镜像和足够磁盘;默认验证 MySQL,`MYSQL_COMPAT_ENGINES="mysql mariadb"` 才验证两种引擎。它不能代替 v17 真实旧数据升级与 v18 字段级兼容检查,正式环境不得用生产数据试验重入。 + +提交前发现的问题可用下面的聚焦测试复核,不需要执行全量验收: + +```bash +# Agent 包地址配置兼容性 +bash scripts/tests/configure_agent_package_test.sh + +# Footstone 到严格 TMS 的 JSON 与 multipart 签名 +cd manager/backend +go test ./services/footstone/infrastructure/repository/tmsclient + +# Repository 详情切换和 OCAI 首次会话路径 +cd ../frontend +npm run test:repository +``` + +同步任务的容量用例必须同时覆盖两种模式:Include 非空时按 `REPOSITORY_FILTERED_SYNC_BYTES/PACKAGES`(默认 `2 GiB / 1000 packages`)估算;Include 为空时按 `REPOSITORY_MAX_SYNC_BYTES/PACKAGES`(默认 `64 GiB / 100000 packages`)估算。仅填写 Exclude 不会缩小下载入口,仍按全量模式处理。管理员可以分别覆盖两组配置,但筛选值不能突破全量硬上限;控制面预检与 Worker 下载前检查必须得到同一模式的预算。 + +默认复用当前仓库的正式 TMS 编排: + +```text +tms/deploy/docker-compose.yaml ++ infra/tms.override.yml ++ config/.env ++ compose project oc-tms +``` + +如现场使用已有的独立 TMS control-root,可显式指定: + +```bash +export OCM_REPOSITORY_TMS_CONTROL_ROOT=/path/to/control-root +``` + +独立 control-root 中应包含 `compose.yaml` 和 `.env`。如需更精确地匹配现有编排,可使用 +`OCM_REPOSITORY_TMS_COMPOSE_FILE`、`OCM_REPOSITORY_TMS_OVERRIDE_FILE`、 +`OCM_REPOSITORY_TMS_ENV_FILE` 和 `OCM_REPOSITORY_TMS_PROJECT_NAME`。 + +`repo-acceptance` 中实际执行的恢复和旧核心链路为: + +```text +环境与 Compose preflight +-> Repository CRUD/Batch 与后端回归 +-> 单 RPM/tar.gz 上传、同步、quarantine 与 NEVRA +-> repodata、只读 HTTP 与动态 .repo +-> Case A/B/C 隔离 DNF 安装 +-> GPG 正向与负向样例 +-> 产品生成 .repo 的隔离 GPG 安装复现(现场/部署后验证入口) +-> 失败诊断、大日志治理、OCAI 实际模型 SSE 响应与报告导出 +-> immutable Revision 节点间分发 +-> restart/redeploy/destroy-restore +-> AC-01 至 AC-31 聚合 +-> 6 个 Core 与 3 个 Enhanced Evidence Bundle +``` + +旧核心层通过时输出如下;它仍不是 152 项结果校验: + +```text +Repository Acceptance +31/31 PASS +0 failed +0 pending +Release Acceptance: PASS +``` + +任一 MUST 失败时,命令退出码非零,不生成 `FINAL READY` 结论。 + +之后严格检查 152 项结果:预期/唯一预期/实际 ID 均为 152,重复、缺失、意外、非 PASS 和不完整引用均为 0。校验通过只说明结果结构与引用符合门禁;测试日期、源码 manifest、真实输出和截图仍需逐项可追溯。当前源码改变后应重跑受影响测试并重新生成结果,不得仅手工把状态改成 PASS。 + +## 3. Evidence 核对 + +验收结果保存在 `OCM_REPOSITORY_RELEASE_OUTPUT` 指定的目录。至少核对: + +下面的 Build100 路径是历史本地原始证据定位,不随源码分发,也不是新环境的必需目录: + +```text +.run/repository-poc/evidence/v4-final-20260904/20260904-build100-full-v3/ +``` + +该次结果为 AC-01 至 AC-31 `31/31 PASS`,6 个 Core 与 3 个 Enhanced 分类全部 `PASS`。 + +```text +recovery/summary.json +recovery/product/summary.json +recovery/tms/summary.json +recovery/SHA256SUMS +release-acceptance/summary.json +release-acceptance/ac-results.json +release-acceptance/category-summary.json +release-acceptance/SHA256SUMS +bundles/summary.json +bundles/presentation.tsv +bundles/core-*/index.json +bundles/enhanced-*/index.json +bundles/SHA256SUMS +``` + +```bash +(cd "$OCM_REPOSITORY_RELEASE_OUTPUT/recovery" && sha256sum -c SHA256SUMS) +(cd "$OCM_REPOSITORY_RELEASE_OUTPUT/release-acceptance" && sha256sum -c SHA256SUMS) +(cd "$OCM_REPOSITORY_RELEASE_OUTPUT/bundles" && sha256sum -c SHA256SUMS) +``` + +对外展示 6 个 Core 和 3 个 Enhanced 分类,旧核心 `ac-results.json` 保留 31 项检查。产品化扩展由固定的 152 项集合记录,二者 ID 不可互换。正式可阅证据见[结构化验收报告](私有化软件仓库结构化验收报告.md)与 `docs/images/repository/`;新部署按本次输出目录生成自己的原始 JSON、命令日志和 SHA-256,不要求访问作者的 `.run`。 + +## 4. 10 分钟现场演示 + +1. 登录 OCManager,进入 Repository,按 OpenCloudOS / 9.6 筛选。 +2. 创建 Base Sync Repository 和 Private Repository,展示 Product/Version 组织。 +3. 触发受控上游同步,展示 include/exclude、架构过滤、Task 和新 Revision。 +4. 上传私有 RPM,展示 NEVRA、包来源和 repodata。 +5. 在 Repository Detail 生成并下载 `.repo`,展示 baseurl 和 GPG policy。 +6. 在干净 OC9 环境禁用默认源,执行 Case C,展示私有包和同步依赖的 source repo。 +7. 上传损坏或错误签名 RPM,展示 quarantine、规则诊断、OCAI 日志分析入口和 active Revision 不变。 +8. 将源 Repository Node 的 immutable Revision 分发到独立 Agent 节点,展示 digest、stdout 和状态;有多个目标时可继续追加选择。 +9. 在节点运维 Drawer 展示多个仓库的包数、内容大小与真实占用,从该行触发同步;展示任务总览、Target 与日志下载。需要演示 Node RPM 时,提前发布包,现场展示安装源、白名单、版本选择和已完成的 TMS 安装任务。 +10. 展示已生成的当前源码验收摘要、152 项结果及 SHA-256,说明 31 项核心和 152 项产品化证据的区别;现场不执行耗时较长的完整恢复测试。多包安装、批量删除和 Inventory Import 可选其中一个流程展开,其余给出真实结果,不用十分钟承诺覆盖所有验收项。 + +### 4.1 历史 Build117 GPG 公钥删除回归 + +Build117 修复了“仓库已经发布过,前端隐藏公钥删除入口,导致空仓库无法完成删除”的交互闭环。验收规则如下: + +| 场景 | 期望 | 结果 | +| --- | --- | --- | +| 活动 Revision 仍有软件包 | 公钥删除被后端拒绝,现有 YUM/DNF 源保持可用 | PASS | +| 活动 Revision 存在但包数为 0 | 公钥可以删除,随后仓库可以按原删除流程删除 | PASS | +| 前端 GPG 页 | 删除按钮始终显示,并明确说明有活动包时会拒绝 | PASS | +| GPG 正负向链路 | 授权签名安装、未签名/未授权隔离、远程同步与私钥边界保持通过 | PASS | + +聚焦 Evidence 位于: + +```text +.run/repository-poc/evidence/20260910-build117-gpg-delete/ +``` + +`summary.json` SHA-256 为 `bf1c0240ba977e0f4f2685c3df9c0806a44d5d5a5b86ef68fe7db027a06ec0b2`。该结果是 Build117 的聚焦回归,不替代当前工作树的完整 Release Acceptance。 + +### 4.2 历史 Build118 隔离项删除回归 + +Build118 将隔离项从仓库删除阻断条件中移除。删除确认仍检查活动 Revision 软件包、未完成上传、GPG 公钥和运行中任务;上述条件均满足时,在同一数据库事务中先清理该仓库的隔离元数据,再删除仓库定义。 + +| 检查 | 结果 | +| --- | --- | +| 仅存在隔离项的空仓库可以删除 | PASS | +| 隔离元数据与仓库定义在同一事务中处理 | PASS | +| 活动软件包仍阻断删除 | PASS | +| 前端不再查询或显示“隔离项阻断删除” | PASS | +| Repository 前端测试 | 28/28 PASS | +| Vite 正式构建 | PASS | +| OpenCloudOS 9 Repository Service 测试与 `go build ./...` | PASS | + +该改动不放宽活动内容、公钥和运行任务保护,也不改变历史 Revision 与任务审计记录的保留策略。 + +### 4.3 历史 Build119/Build122 上游筛选回归 + +| 检查 | 结果 | +| --- | --- | +| include 在网络下载前生效 | PASS | +| OpenCloudOS 9.6 BaseOS 仅下载 `curl` 一个 RPM | PASS | +| 下载后 RPM/架构/include/exclude 二次校验 | PASS | +| 生成不可变 Revision、repodata 和 HTTP 发布地址 | PASS | +| BaseOS/AppStream 快捷包组按真实来源区分 | PASS | +| 未核验版本、架构和自定义源不显示猜测性快捷项 | PASS | +| 内置源默认进入筛选模式,空 Include 阻止提交 | PASS | +| Repository 前端测试 | 29/29 PASS | +| 本轮 Repository 变更文件 ESLint、全量 Stylelint、Vite 正式构建 | PASS | + +该历史试验使用过 512 MiB 受控上限,不是当前产品默认值。现行 Include 非空预算为 `2 GiB / 1000 packages`、完整同步预算为 `64 GiB / 100000 packages`,均可由管理员显式配置并继续执行真实容量门禁;完整源只能在资源满足时执行。历史聚焦 Evidence 定位为 `.run/repository-poc/evidence/20260910-build119-filtered-sync/`,原始文件不随源码分发。 + +Build118 Evidence 位于 `.run/repository-poc/evidence/20260910-build118-quarantine-delete/`,目录内 `SHA256SUMS` 已完整校验。 + +### 4.4 历史 Build124-Build126 架构筛选与排队预检回归 + +OpenCloudOS 9.6 的内置快捷项已扩展到 `x86_64`、`aarch64` 和 `noarch`。新增产品级矩阵覆盖 AArch64 BaseOS 2 组、AArch64 AppStream 2 组、Noarch BaseOS 1 组和 Noarch AppStream 2 组;每组都通过真实产品 API 创建仓库、远程下载、包清单/架构核对、不可变 Revision、标准 repodata、动态 `.repo` 和 HTTP 读取检查。 + +Noarch 上游没有独立目录。验证从 x86_64 与 aarch64 元数据入口分别下载 `dnf-4.16.2-5.oc9.noarch.rpm` 和 `container-selinux-2.234.2-1.oc9.noarch.rpm`,两组文件的 SHA-256 分别一致;产品固定使用 x86_64 元数据入口,同时 worker 通过 `--forcearch=noarch`、`--arch=noarch` 和 RPM 二次校验只接收 noarch 包。 + +首轮并行矩阵有 1 个任务因等待超过 5 分钟触发 `REPOSITORY_NODE_PRECHECK_STALE`,由此补充了领取阶段的预检刷新。Build124 的单元测试和隔离 MySQL 集成测试均通过;第二轮真实任务中,排队超过 TTL 的 AArch64 任务获得新的 `preflight_id` 并成功发布,证明刷新不是单纯延长超时。 + +第二轮矩阵最终结果为 `7/7 PASS`,共同步并发布 25 个真实 RPM、`38,288,545` 字节;其中 2 个任务在领取时生成了新的预检记录。所有用例都保存了 Repository、Task、Package Inventory、动态 Repo Config、`repomd.xml` HTTP 响应和汇总结果,并由 `SHA256SUMS` 完整校验。 + +随后使用干净的 OpenCloudOS 9.6 最小容器执行消费侧复核。测试禁用全部默认仓库,只挂载产品生成的 7 个 Repo Config,对 25 个目标包逐一执行 `dnf repoquery`:Noarch 三组结果全部为 `noarch`,AArch64 四组结果只出现 `aarch64/noarch`,每条结果的 `repoid` 都指向对应的 OCManager 不可变 Revision。该检查用于证明元数据可被 DNF 解析且包来源可信;受当前硬件限制,AArch64 用例不表述为原生 AArch64 安装。 + +| 检查 | 结果 | +| --- | --- | +| AArch64 BaseOS/AppStream 四组快捷包 | PASS | +| Noarch BaseOS/AppStream 三组快捷包 | PASS | +| 包名、来源与允许架构精确匹配 | PASS | +| Noarch 跨元数据入口 SHA-256 一致 | PASS | +| 排队预检过期后按最新节点状态刷新 | PASS | +| 标准 repodata、不可变 Revision 与 HTTP 发布 | PASS | +| Repository 前端测试 | 29/29 PASS | +| 本轮 Repository 变更文件 ESLint、全量 Stylelint、Vite 正式构建 | PASS | + +![当前 Noarch 来源与包组联动操作示例,历史实验结果见上表](images/repository/repository-current-20261024-create-noarch-filter.png) + +图中 `noarch` 仓库使用固定元数据入口,快捷列表只显示当前来源已经核验的平台无关包组;切换到自定义来源后仍可直接输入实际包名或 glob。 + +同步与发布 Evidence 位于 `.run/repository-poc/evidence/20260910-build125-architecture-matrix/`,消费侧 DNF Evidence 位于 `.run/repository-poc/evidence/20260910-build127-architecture-consumer/`,两处 `SHA256SUMS` 均已在远端和本地完整校验。这里的“矩阵全量”指已提供的来源、架构和快捷包组组合全部覆盖,不表示在测试节点完整镜像整个发行版仓库。 + +前端全量 Stylelint、Repository 专项测试和 Vite 正式构建均通过;本轮新增或修改的 Repository 文件也通过聚焦 ESLint。上游仓库其余历史文件的全量 ESLint 基线仍有既有问题,因此不将“全仓 ESLint”写成 PASS,也不在本功能中批量格式化无关文件。 + +### 4.5 历史 Build128 任务日志 lease 恢复回归 + +真实同步任务进入 RUNNING 后,测试主动停止 Footstone 25 秒,再启动同一管理面。Worker 在 lease 失效后重新领取同一任务,任务保持原 ID,以 `attempt=2` 继续执行并最终发布成功。此前旧执行日志占用固定路径时可能触发 `TASK_LOG_INIT_FAILED`;Build128 将上一轮日志和元数据原子移入任务目录下的 `recovered/`,新的 attempt 继续使用稳定的当前日志 API 路径。 + +| 检查 | 结果 | +| --- | --- | +| 控制面中断期间任务不误发布 | PASS | +| 同一任务 lease 回收并进入 `attempt=2` | PASS | +| 旧 attempt 日志和 metadata 保留在 `recovered/` | PASS | +| 当前日志 API 可读取最终执行日志 | PASS | +| 未出现 `TASK_LOG_INIT_FAILED` | PASS | +| 新 Revision 生成标准 repodata | PASS | +| 不可变发布地址返回 HTTP 200 | PASS | +| 两个 Repository Worker 节点部署 Build128 并健康运行 | PASS | + +本次任务发布 Revision `ada8fdf1-9696-4310-ab0f-cd07b84259ca`,执行日志压缩后为 726 B,上一 attempt 日志压缩后为 415 B;两者未写入 MySQL。Evidence 位于 `.run/repository-poc/evidence/20260911-build128-tasklog-recovery/`,控制面和两个节点的 `SHA256SUMS` 均已校验。 + +### 4.6 Build129 历史候选完整验收 + +2026-09-11 从本地主仓生成完整 Build129 版本化快照,在 OpenCloudOS 9.6 管理节点重新执行 `make -C manager repo-acceptance`。源码包上传前后 SHA-256 一致,关键后端、前端、脚本和中文文档又逐文件比对;远端快照不包含 `.git`、`.run`、`node_modules`、构建产物或私有 `.env`。 + +首轮构建 Footstone 成功,但当时配置的内网镜像地址 DNS 不可达,Worker 基础镜像 metadata 获取失败。随后复用刚完成 Build128 源码测试的 Worker 镜像与本轮新构建的 Footstone 镜像,在独立 r3 输出目录执行完整验收。该处只规避外部镜像站故障,不复用功能测试结果;该 Build 源码的后端、前端、DNF、GPG、日志、OCAI、分发和恢复仍在本轮重新执行。 + +提交前又将 Repository Worker 的默认基础镜像收敛为 OpenCloudOS 官方固定标签 `docker.io/opencloudos/opencloudos9-minimal:9.6-v20260714`。本地缓存构建 POC 确认该镜像为 OpenCloudOS 9.6;联网环境可从官方仓库拉取,隔离网可按部署文档离线导入。 + +| 分类 | 结果 | +| --- | --- | +| Repository Management | 7/7 PASS | +| Package Ingestion | 7/7 PASS | +| Repository Publishing | 3/3 PASS | +| DNF Validation | 4/4 PASS | +| Automation | 6/6 PASS | +| Security | 1/1 PASS | +| Distribution | 1/1 PASS | +| Diagnosis | 1/1 PASS | +| Repo Generator | 1/1 PASS | +| AC-01 至 AC-31 | **31/31 PASS** | +| Release Acceptance | **PASS,final_ready=true** | + +本轮 TMS 重启后,两个独立 Agent 均重新连接并返回任务输出。产品分发把 Repository `0307c65c-fb56-4ce8-ad0f-837777e0d2a9` 的不可变 Revision `b472aa70-7364-4bcf-8470-94c8b17797e0` 从主仓库节点下发到独立目标 Agent,manifest、repomd 和原子切换全部 PASS。GPG 正向安装、未签名/未授权 key 隔离、Case A/B/C、真实 OCAI SSE 和三层恢复同时通过。 + +最终 Evidence 位于 `.run/repository-poc/evidence/v4-final-20260911/build129-release-candidate-r3/`。目录包含 159 个文件,所有 `SHA256SUMS` 和 JSON 均已复核;回传压缩包 SHA-256 为 `f2386ac6d5e069a57018cc52ba3245cfd49d2cc8604dc643cafb0e15d3b7f6fe`,凭据特征扫描为 0 命中。 + +### 4.7 历史 Build130 官方 OC9 镜像完整验收 + +Build130 将 Repository Worker 默认基础镜像固定为 `docker.io/opencloudos/opencloudos9-minimal:9.6-v20260714`,随后重新生成完整源码快照并执行总验收。本轮没有设置 `P12_REUSE_IMAGES`:Footstone 与 Repository Worker 均从该 Build130 源码重新构建,Worker 构建直接使用新的官方 OC9 9.6 默认值。 + +源码压缩包上传前后 SHA-256 为 `e7b4141d7fb357a6026063809e19e56c1ec4da9b0f4de93053d90b4a9de8d47d`。三层恢复、TMS 单目标与多目标、后端回归、前端 29 项测试与正式构建、Case A/B/C、32 类诊断、1 GiB 日志、真实 OCAI SSE、GPG 正负向和产品分发全部重新执行,最终仍为 `31/31 PASS`、9/9 分类 PASS、`final_ready=true`。 + +最终完成依据更新为 `.run/repository-poc/evidence/v4-final-20260911/build130-official-oc9-image-r1/`。目录包含 162 个文件,全部内部 SHA-256 清单和 JSON 解析为 0 失败,凭据特征扫描为 0 命中;回传压缩包 SHA-256 为 `5327f2841a89cbfa9474a881b3d41fa62dba686af5a249270ef1337ce6412f5f`。 + +### 4.8 历史 Build141 同步过滤与上传终态回归 + +Build141 在不修改数据库 schema、Repository RPC 协议和任务状态机的前提下,增加同步过滤容错、上传失败终态收敛和国内镜像快捷来源。该 Build 源码部署到一台管理节点和两台 Repository Worker 节点,并完成以下聚焦回归: + +| 检查 | 结果 | +| --- | --- | +| include 同时包含有效包和不存在包 | PASS:有效包发布,任务 `SUCCESS` | +| 未匹配包诊断 | PASS:任务日志产生 `WARN`,包含 `unmatched_packages` | +| include 全部不存在 | PASS:任务 `FAILED`,错误码 `SYNC_EMPTY_RESULT`,active Revision 不变 | +| 上传在 GPG 公钥同步前置检查失败 | PASS:任务失败且对应 Upload 收敛到 `FAILED` | +| 失败 Upload 后删除空仓库 | PASS:不再被残留 `READY/PROCESSING` 记录阻断 | +| 国内 OpenCloudOS BaseOS/AppStream 内置源 | PASS:与官方源逐项保持版本、架构和包组约束一致 | +| Repository Worker、Repository Service、Content Service、Application 单元测试 | PASS | +| 上述 Go 包 `go vet` | PASS | +| Repository 前端专项测试 | **30/30 PASS** | +| 全量 Stylelint 与 Vite 正式构建 | PASS | +| 新增 Repository 选项文件和新 OCAI 列表组件聚焦 ESLint | PASS | + +混合同步用例选择 BaseOS 中真实存在的 `bash` 和故意不存在的 `ocm-package-does-not-exist`。发布清单只包含 `bash`,不存在包不会让已匹配内容回滚,也不会被误写入 Repository;全未匹配用例则 fail closed,不生成空的替代 Revision。上传负向用例使用 `VERIFY_PACKAGE` 策略但未配置授权公钥,确认任务和 Upload 两个对象得到相同的 `GPG_KEY_SYNC_REQUIRED` 失败结果,随后通过产品删除接口完成清理。 + +Build141 当时记录了 4 个 OpenCloudOS 内置来源、来源联动包组和保留的自定义输入。下面用 2026-10-24 当前页面重新说明同一操作入口,不表示重跑 Build141;该阶段原始结果继续保留。Build141 时全仓 ESLint 包含上游历史文件的既有规则问题,因此当时只把修改文件的聚焦 ESLint、全量 Stylelint、Repository Vitest 和正式构建列为通过项。 + +![当前四个 OpenCloudOS 内置来源,Build141 结果保留在上文](images/repository/repository-current-20261024-sources.png) + +![当前创建仓库的来源选择](images/repository/repository-current-20261024-create-upstream.png) + +![当前来源联动包组与自定义输入](images/repository/repository-current-20261024-create-filter.png) + +Build141 当时的结论为“增量聚焦回归通过”,因此该阶段不能替代 Build130 的完整 Release Acceptance。后续上游合并和完整 `make -C manager repo-acceptance` 已由 Build143 完成,结果见 4.10 节。 + +### 4.9 历史 Build141 上游集成预演 + +2026-09-12 以当时最新的上游 `master`(`853dba6`)创建隔离 worktree,对已提交功能基线和 Build141 未提交增量执行无提交合并。25 个两侧共同修改文件中产生 11 个文本冲突,均逐文件保留上游 JWT/HMAC、CA 安全和部署更新,并接入 Repository 路由、服务、端口及脚本能力;最终未解决冲突为 0。 + +| 检查 | 结果 | +| --- | --- | +| Git 未解决冲突、暂存区 diff check | PASS | +| Linux 源码归档发布检查 | PASS:1448 个文件,Shell、JSON、文档图片、凭据和可移植性检查通过 | +| Repository Vitest | 6 个文件,30/30 PASS | +| 全量 Stylelint、Vite release build | PASS | +| Repository 选项、OCAI 列表和路由聚焦 ESLint | PASS | +| OC9 Repository Worker、Repository Service、Content Service 测试 | PASS | +| OC9 Application 与实例查询冲突相关测试 | PASS | +| OC9 Repository 相关包 `go vet` | PASS | +| OC9 `go build ./...` | PASS | + +上游 permission 包执行全量 `go vet` 时仍会报告既有 Mutex copy 告警;纯源码目录直接运行全部 `instancerepo` 测试还需要 Footstone 测试配置和数据库环境。因此这里不写成“全仓 go vet/go test PASS”。该历史集成预演只证明代码能够与当时上游合并并通过聚焦回归,完整 `repo-acceptance` 仍是进入 PR 前的最终门禁。 + +### 4.10 历史 Build143 上游集成完整验收 + +Build143 在上述隔离合并候选上补齐两项跨环境约束:恢复脚本允许通过 `P12_HELPER_IMAGE` 指定可达的 Alpine 辅助镜像;`.gitattributes` 固定 `*.spec text eol=lf`,确保 Windows 检出后打包到 OpenCloudOS 9 的 RPM 测试夹具不会携带 CRLF。候选由 Git tree `67da943f37ff5b6a41859e763ed97231996ff7ad` 生成,源码归档 SHA-256 为 `52941ee05e6c1c210b8965bf78c6f722f31f0b77d763e0cc56c9bfe277eb7362`,远端解包后 `repository-release-check.sh` 扫描 1448 个文件并通过。 + +完整验收在 OpenCloudOS 9.6 控制节点的交互 sudo 会话中执行;默认外网不可达时使用进程级 Go Module 代理,JWT 从现有 `0600` 会话文件读取,未写入仓库或 Evidence。结果如下: + +| 检查 | Build143 结果 | +| --- | --- | +| Restart / Redeploy / Destroy-Restore / lease recovery | PASS | +| TMS 全服务重启与请求的 Agent 重连(历史环境为两个目标) | PASS | +| Repository 后端回归 | PASS | +| Repository 前端 | 6 个文件,30/30 PASS;Vite release build PASS | +| Case A/B/C | 3/3 PASS,默认源禁用且来源可追溯 | +| 规则诊断与 1 GiB 日志治理 | PASS;1,073,741,971 bytes 流式压缩,摘要 65,536 bytes | +| 真实 OCAI | `deepseek-v4-flash` SSE 完成、上下文摘要、脱敏和只读边界 PASS | +| GPG | 授权签名安装、未签名/未授权隔离、私钥不落库 PASS | +| 产品分发 | 源 Repository Node 到独立目标 Agent,Revision/manifest/repomd/原子发布 PASS | +| AC-01 至 AC-31 | **31/31 PASS,0 failed,0 blocked,0 pending** | +| 9 类 Evidence Bundle | **9/9 PASS** | + +本地 Evidence 位于 `.run/repository-poc/evidence/v4-final-20260912/build143-upstream-integration-r1/`;回传归档 SHA-256 为 `f3e598e58b087456163a26799c122d0c87db6edae05d9b57107cf9981f9c1c4c`。Build141 之后到 Build143 未改变 Repository 页面交互,因此 Build141 重新采集的仓库列表、四个上游来源、创建表单和筛选截图可作为该历史完整验收对应的页面证据。 + +### 4.11 历史 Build152 干净 OpenCloudOS 9 从零部署复核 + +2026-09-12 在独立新建的 OpenCloudOS 9.6 x86_64 虚拟机中,使用空 Docker 数据目录和空 OCManager 数据卷执行从源码部署。候选源码树为 `dbed251f44e4a861f6e793bbf047de3ec6d12b75`,归档 SHA-256 为 `218b0e49c4f4fb49f69452284d50ce24d4ddedaf7e188514fc8c0ea6b36e3e50`;解包后重新校验哈希,发布检查扫描 1454 个文件并通过。 + +首轮部署在 MySQL 首次初始化阶段稳定暴露 `ERROR 1045`:容器健康检查已经通过,但临时 server 尚未完成最终 root 凭据初始化。部署脚本现改为循环执行真实鉴权查询并确认 `@@port > 0`,迁移失败立即停止;Repository 增量迁移同时按 MySQL 8.0.35 做了首次导入和重复导入验证。前端构建访问官方 npm registry 时出现 `ECONNRESET`,因此增加独立的 `NPM_REGISTRY` 配置、连通性预检和有限重试;该调整没有修改 `package-lock.json` 或升级依赖版本。 + +| 检查 | Build152 结果 | +| --- | --- | +| 干净环境检查 | **42/42 PASS**,Docker、Compose、OpenCloudOS 9、磁盘、端口及三类网络源分别检查 | +| MySQL 8.0.35 迁移 | 首次初始化与重复执行 PASS,未再出现初始化窗口竞态 | +| 全栈部署 | `scripts/deploy.sh up` PASS;Manager 健康检查 **17/17 PASS** | +| 后端聚焦回归 | Repository、迁移、部署门禁与网关签名测试 PASS | +| 前端回归 | 6 个文件、**30/30 PASS**;Vite release build PASS | +| 真实页面 | 登录、首页、主机页、仓库列表、仓库节点、上游来源和创建仓库表单均可访问,控制台 0 error/0 warning;主机列表三个查询接口均为 HTTP 200 | +| Case A/B/C | 3/3 PASS,默认软件源禁用,Repo Config 和软件包来源可追溯 | +| GPG | 授权签名安装、未签名/未授权隔离及私钥不落库 PASS | +| 规则诊断与日志治理 | 32/32 规则 PASS;1 GiB JSONL 流式压缩和 64 KiB 摘要 PASS | +| 产品恢复 | restart、应用 redeploy、disposable destroy/restore、lease、mTLS、只读发布和状态摘要一致性 PASS | +| Core Acceptance | **27/31 PASS,0 FAIL,3 BLOCKED,1 IN_PROGRESS** | + +本轮非 PASS 项来自干净虚拟机只部署了基础控制面,不是把失败隐藏为成功:AC-24 缺少可用 OCAI SSE 会话;AC-25 缺少真实 Repository Node、immutable Revision 和任何独立目标 Agent;AC-29 已完成产品数据恢复,但没有执行带 Agent 的 TMS 恢复;AC-31 因前述 MUST 未全部通过而保持 `BLOCKED`。这些可选组件未部署时,42 项环境预检、16 个基础容器、17 项健康检查、登录和主机查询均正常,证明基础组件具备独立部署与降级运行能力。Build152 不替代 Build143 在完整功能拓扑上的 `31/31 PASS` 发布基线。 + +部署日志中的剩余告警为 disposable 环境使用公开默认密码,以及锁定前端依赖树中的 deprecated/audit 提示。前者在生产部署前必须通过 `config/.env` 替换;后者未使用 `npm audit fix --force` 做破坏性升级,当前 Repository 测试、正式构建和真实页面均不受影响。 + +Build152 的 Evidence 由运行时的 `OCM_REPOSITORY_RELEASE_OUTPUT` 和各细粒度 `*_OUTPUT` 变量指定,并在导出后通过 `SHA256SUMS` 核对。文档不固化测试机目录、账号、IP 或跳板机名称。 + +### 4.12 历史 Build154 基础拓扑复核 + +Build154 对应提交 `1fa9b1e3e61b55cd4459e27904e2da37da302d05` 的 Git tree `a23037dd6098ee4e89c0ad18b62b5f7f297f380a`。候选归档上传 OpenCloudOS 9.6 后重新核对 SHA-256,发布检查扫描 1456 个 Git 文件并通过。 + +本轮不启用 OCAI、Repository Worker、Repository Publisher 或 Agent。环境预检 `42/42 PASS`,infra、TMS 和 manager 共 16 个基础容器运行,健康检查 `17/17 PASS`,前端 HTTP 200。后端测试、`go vet`、`go build ./...`、MySQL 8.0.35 迁移兼容测试、Repository Shell 测试、前端 Repository/路由 `28/28` 测试和正式构建均通过。该结果验证当前主线的基础部署与可选组件隔离,不替代 Build143 的完整功能历史 Evidence。 + +## 5. 运行边界 + +- 整套 TMS 只在无进行中分发任务时重启。 +- 负向试验已确认,任务执行中重启全部 TMS 服务可能返回 `sent_ack_timeout`;此场景不视为无损恢复。 +- 分发失败后通过产品重试,Repository worker 任务由 lease recovery 恢复。 +- Evidence 中不保存私钥、真实凭据或用户密码。GPG POC 私钥只在临时目录并在结束后清理;正式 Metadata Signing Key 则按部署说明留在 Node 本地受限目录,不随 Inventory 或日志导出。 + +## 6. 提交材料与方式 + +提交内容分为代码与展示材料两部分: + +| 材料 | 内容 | 提交方式 | +| --- | --- | --- | +| 项目代码 | Repository 后端、前端、部署、自动化脚本及相关 Markdown 文档 | 向 `OpenCloudOS/ocmanager` 提交 Pull Request | +| 方案文档 | 系统、数据库、接口、部署、使用和时间规划 | 随代码 PR 提交 Markdown 文件 | +| 测试与验收文档 | Release Acceptance、Case A/B/C、GPG、恢复和复现步骤 | 随代码 PR 提交 Markdown 文件 | +| 运行截图 | 仓库页面、任务、隔离区、OCAI、分发和验收结果 | 作为 Evidence 整理后通过邮件发送 | +| 结构化验收报告 | 正式 Markdown、`summary.json`、152 项结果、分类报告和 SHA-256 | Markdown 随 PR;脱敏后的必要汇总和截图随仓库,完整原始 Evidence 可作为附件发送 | +| 简短演示视频 | 从建仓、同步/上传到安装、诊断和分发的主要流程 | 通过邮件发送 | + +提交前检查代码 PR 中不包含 API Key、密码、私钥、运行时数据库、RPM 大文件或临时 Evidence。邮件材料中的截图和报告也必须先执行凭据特征扫描。 + +## 7. 当前产品化功能与 152 项结果 + +机器预期集合固定在 `manager/acceptance/repository-acceptance-criteria.json`,包含 `1..148`、`23A`、`23B`、`142A` 和 `TEACHER-NODE-01`,共 152 个唯一 ID。Markdown 只负责说明,不能代替该 JSON 推导预期集合。以下流程以当前代码和接口为准;第 4 节的历史实验保留用于说明已处理的问题,不作为当前页面的使用说明。 + +### 7.1 关键产品流程 + +下表列出的页面文件是当前操作示例,配合第 16 节机器结果和原始测试证据阅读;页面截图本身不替代真实执行、来源校验或安全测试。 + +| 流程 | 验证内容 | 最终候选证据 | +| --- | --- | --- | +| Node 运维视角同步 | Node Drawer 展示容量、Repository、逻辑/物理占用、Revision、同步状态和最近任务,并复用原 Sync API | `repository-current-20261024-node-drawer.png` | +| 多包多目标安装 | 一个 Package Install Task 包含多个 Target,每个 Agent 独立 Preview 和执行 | `repository-current-20261024-package-install-preview.png`、`repository-current-20261024-package-install-result.png` | +| 批量删包 | 一次 Preview、一个 DELETE_PACKAGE Task、逐包 Result Item、一次 repodata rebuild | `repository-current-20261024-batch-delete-preview.png`、`repository-current-20261024-batch-delete-result.png` | +| Inventory Import | Preview/Confirm 绑定摘要和资源版本,统一进入 Global Task | `repository-current-20261024-inventory-import-preview.png`、`repository-current-20261024-inventory-import-task.png` | +| 双架构隔离 | x86_64 与 aarch64 仓库、Artifact 和节点匹配,不允许跨架构选择 | 当前界面示例:`repository-current-20261024-node-drawer.png`、`repository-current-20261024-arm-node-drawer.png`;真实安装/匹配结果见第 15、16 节 | +| 同步计划与资源控制 | 每日/每周、跳过已有同步、唯一 Run Key、失败自动暂停;MIRROR/APPEND、下载限速和并发实际生效 | Schedule/Run、Task 实际配置、受控源下载日志 | +| 多文件断点上传 | 每文件独立 Session,乱序和幂等 Chunk,暂停/恢复、刷新后重选同文件补缺片 | Session/Chunk 响应、文件 SHA-256、浏览器操作 | +| GPG 双层策略 | RPM DISABLED/AUDIT/ENFORCE;Metadata 签名、公钥和动态 `repo_gpgcheck` 保持一致 | RPM/隔离项、`repomd.xml.asc`、GPG/DNF 输出 | +| Node RPM 产品源 | 标准 Private Repository + repodata,安装源/Artifact/EVR 选择/架构复核 | 安装源 API、immutable URL、DNF 实际来源 | +| 自动注册安全 | 双重白名单校验、Token 一次消费、精确请求恢复、撤销竞争与证书生命周期 | Enrollment/公开证书、事务与并发测试、Target 结果 | +| 全局运维 | 任务四态统计、完整筛选集节点 Summary、统一中文错误、后端脱敏日志 Bundle | API 响应、tar.gz 清单、截图和 Secret 扫描 | + +![Node 运维视角同步](images/repository/repository-current-20261024-node-drawer.png) + +![多包多目标安装结果](images/repository/repository-current-20261024-package-install-result.png) + +![批量删除单任务结果](images/repository/repository-current-20261024-batch-delete-result.png) + +![Inventory Import 全局任务](images/repository/repository-current-20261024-inventory-import-task.png) + +### 7.2 按功能复现的前置条件与通过标准 + +| 功能 | 操作前提与入口 | 操作与通过标准 | +| --- | --- | --- | +| Node 运维 | 一个有心跳的 Repository Node,至少承载两个仓库 | 打开节点 Drawer;仓库数、包数、logical/physical 分列;点击有上游仓库的同步,生成原 Sync Task,完成后刷新 Revision;最近任务跳转后带 Node Filter | +| Task 聚合 | 同项目存在不同状态、类型、触发方式与 Target 的任务 | 以 `created_at` 筛选;切换 `FAILED` 不改变顶部四态总数,点击统计才改变列表;Target 失败优先,开始/结束与错误可追溯 | +| Schedule | 一个有上游且可成功同步的仓库,至少两个调度实例用于幂等试验 | 创建每日/每周计划;同一 scheduled_at 只生成一个 Run/Task;已有 Sync 则 SKIPPED;连续失败达到阈值 AUTO_PAUSED,显式恢复后按规则重置 | +| 限速与模式 | 可控上游、有足够包大小与连接日志,预算/空间均通过 | 设置带宽与并行数后实测而非只查配置;保存 Task 实际参数。上游删除一个包再分别执行 MIRROR/APPEND,确认前者移除、后者保留 | +| Indexed Chunk | 允许上传的私有仓,测试文件小于已配置单文件/展开上限 | 一个大文件与多个不同大小文件;乱序 Put、相同 hash 重传成功、同 index 不同 hash 拒绝;暂停/断网/刷新重选后仅补缺片;总体进度按字节计 | +| 批量依赖删除 | immutable Revision 内构建 A←B←C 反向依赖链,宿主默认源含干扰包 | Preview 只启用绑定 Repository Set,显示直接和传递影响;默认取消,不静默级联;Confirm 摘要/版本/有效期校验;只生成一个任务和一个 Revision,逐包 Result Item 可查 | +| Package Install | 一个或多个在线 Agent,存在可安装包的 immutable Revision | 每 Target Preview 分别返回 selected/dependency/upgrade/replacement、下载/磁盘变化与 raw output;只允许 READY Target Confirm;Preview 后切换 current,执行仍使用原 Revision;目标结果独立 | +| 包签名策略 | 授权签名、未签名、错误 Key 签名和损坏 RPM | `DISABLED` 不开启包签名强制,但结构/架构仍校验;`AUDIT` 保留有效包并记录签名问题;`ENFORCE`(兼容 `VERIFY_PACKAGE`)拒绝不符策略包进入活动源;错误包隔离、active 不变 | +| Metadata Signing | Node 报告 AVAILABLE Key,公钥可发布,私钥只在 Node 受限目录 | 新 Revision 有 repomd、asc 和公钥,GPG/DNF 验证;缺文件时不输出 `repo_gpgcheck=1`;切换 Key 只影响新 Revision,不重写历史 | +| Node RPM | 按部署说明在对应 OC9 架构构建、上传、发布并配置正式安装源 | 在“仓库节点 → 安装仓库节点”完成安装源/Artifact 登记与白名单;默认按 EVR 选最新版、支持历史版本,跨项目/解绑源/普通仓/LEGACY_LOCAL 不入默认;TMS 通过临时源真实 dnf 安装并清理配置 | +| Bootstrap | 目标 Agent 已授权,相关 CA/服务已启用,Node 私钥与 CSR 本地生成 | 正向上线;未授权、过期、身份/架构不符、普通 Replay 拒绝;精确 authorization+CSR+身份重试返回同证书;分别验证撤销先提交与授权先提交的锁顺序 | +| Inventory | 一个 v1/v2 清单以及与当前项目的节点/Key 映射 | Preview 显示 CREATE/SKIP/CONFLICT/INVALID;Confirm 校验摘要/版本/有效期,创建 INVENTORY_IMPORT Task;导出无 Secret,导入不假定恢复 RPM/Revision/任务历史 | +| 日志与诊断 | 失败 Task、Node 可读取原始压缩日志,OCAI 配置可用 | Drawer 显示错误 Code/中文原因/建议;下载后端 `repository-task--logs.tar.gz`,内含 context、summary、JSONL.zst 和 SHA256SUMS,全部脱敏;OCAI 建议来自同一任务上下文 | + +这些流程分别对应当前服务与现有测试,不要求为每个流程另造一套远程执行框架。API 名称与请求结构以 `manager/backend/protocol/repository/repository.proto` 为准,具体字段和前端操作见[方案设计](私有化软件仓库方案设计.md)及[结构化验收报告](私有化软件仓库结构化验收报告.md)。 + +测试数据库必须覆盖空库安装、v17→v18、旧数据兼容与迁移重入。v18 已经应用后不得修改原文件;新的结构修复使用后续迁移。测试前备份数据,测试脚本只操作自己创建的 disposable 数据库,不能在生产库反复试验 DDL。 + +恢复测试必须记录 Repository/Revision、manifest digest、包清单、GPG 元数据与验证报告,并在 restart、redeploy、destroy/restore 后重跑相关验收。现行 mTLS 与 Node 身份必须可恢复;Worker lease/PREPARED 对账不能以重新建仓或删除任务代替。 + +### 7.3 机器验收命令 + +```bash +# 预期集合结构检查 +make -C manager repo-acceptance-criteria + +# 结果校验器正负向测试 +make -C manager repo-acceptance-results-test + +# 只检查本次结果文件(不运行产品测试) +bash scripts/repository-acceptance-results.sh \ + --results "<本次152项结果JSON文件>" --strict + +# 完整总入口:先恢复/核心链路,再检查本次152项结果 +export OCM_REPOSITORY_ACCEPTANCE_RESULTS="<本次152项结果JSON文件>" +make -C manager repo-acceptance +``` + +严格模式检查: + +```text +expected IDs = 152 +unique expected IDs = 152 +actual IDs = 152 +duplicate IDs = 0 +missing IDs = 0 +unexpected IDs = 0 +non-PASS IDs = 0 +failed_count = 0 +exit code = 0 +``` + +每个结果条目必须同时给出非空的 `implementation`、`tests` 和 `evidence`。结果校验器检查集合、状态和三类引用是否齐全,不执行这些测试,不检查所有引用文件的实际存在性,也不判断截图能否证明业务结果。因此制作结果文件时还必须审计引用存在性,并核对源码 manifest、测试退出码、日志摘要和截图含义;不能拿历史结果文件在另一版本上直接声称 152 项重新通过。最终候选冻结源码完成全功能、安全、恢复和双架构验证;文档收口只更新文字、截图和 Evidence 链接,并通过前后 SHA-256 清单确认核心业务代码没有变化。 + +### 7.4 已保存的最终候选结果 + +以下是 2026-10-14 已保存的候选验收记录,不是阅读本文或运行 JSON 校验器时自动产生的新测试结果: + +```text +Full Recovery POC PASS +TMS Full Restart Recovery PASS +Backend Regression PASS +Frontend Vitest 48/48 PASS +Vite Production Build PASS +DNF Case A/B/C 3/3 PASS +Diagnosis Rules 32/32 PASS +1 GiB Log Governance PASS +OCAI SSE PASS +GPG Verification PASS +Two-target Distribution PASS +Core Matrix 31/31 PASS +Evidence Bundle 9/9 PASS +Acceptance Manifest 152/152 PASS +``` + +严格结果中 `duplicate_ids`、`missing_ids`、`unexpected_ids`、`non_pass_ids`、`failed_count` 和 `incomplete_evidence_ids` 均为 0,总入口退出码为 0。 + +公开[候选验收摘要](evidence/repository-acceptance-20261014.json)保存原始汇总的 SHA-256、152 项实现/测试引用和证据摘要校验值;原始环境日志在本地归档。这份只读摘要不是传给 `--results` 的本次运行结果,也不能证明另一环境重新测试通过。 + +## 12. 独立审查修复候选的定向复核 + +本节记录 2026-10-23 开始的定向修复和验证,结果见[修复专项摘要](evidence/repository-review-fixes-20261023.json)。2026-10-25 修复候选以 `ddbdfea457644ed7d01e6c209e9dfd2168cd6953` 更新至 Gitee `master` 与原 PR #420,PR 当前等待上游评审。本节定向结果不替代 2026-10-24 已完成的 152 项全量验收。 + +### 12.1 可复用检查入口 + +在 OC9、Repository Root 中准备已有文档要求的 Go/Node 和系统工具。前端先进入 `manager/frontend`,按锁文件安装依赖,再执行: + +```bash +npm ci +npm run test:repository +npm run test:operations +npm run typecheck:repository +npm run build +``` + +`test:operations` 使用独立配置验证命令助手回填、已安装 TDesign 抽屉和磁盘容量范围,不启动开发 Mock watcher。它不替代 `test:repository`,也不意味着所有旧页面的 lint/typecheck 均无历史问题。已有大 chunk 提示保留,不用调高阈值隐藏告警。 + +从 Repository Root 执行 Shell 契约测试: + +```bash +bash scripts/tests/repository_gpg_install_demo_test.sh +bash scripts/tests/health_check_tms_auth_test.sh +``` + +前者需要 bash、jq、awk 及基础工具,以受控命令桩验证双公钥下载顺序、首次 makecache 和公钥失败立即停止;不验证真实签名。后者还需要 python3/curl,使用临时目录和本地探测端口验证三种 HMAC 健康状态,不连接真实 TMS。 + +真实数据库测试必须使用**可丢弃的全新测试库和独立最小权限账号**,不得提供生产 DSN;用例会建表和清理自己的固定测试行。数据库连接由部署人在私有 Shell 中输入,不写文件或日志。从 `manager/backend` 执行: + +```bash +read -r -s -p '专用临时 MySQL 测试 DSN: ' OCM_REPOSITORY_TEST_DSN +printf '\n' +export OCM_REPOSITORY_TEST_DSN +go test -tags=integration -count=1 -v \ + ./services/footstone/domain/repositoryTaskService \ + ./services/footstone/domain/repositoryNodeService \ + ./services/footstone/application \ + -run 'TestTaskLeaseLockAndRecoveryWithMySQL|TestListRefreshesOfflineSummaryAndHeartbeatWithMySQL|TestReportRepositoryNodeTaskFailureOverMTLSWithMySQL' +unset OCM_REPOSITORY_TEST_DSN +``` + +DSN 格式遵循 MySQL 驱动:`:@tcp(:)/?charset=utf8mb4&parseTime=True&loc=Local`。专用库/用户由测试管理员创建并在核验证据后清理,命令本身不会创建数据库;未设置 DSN 的 SKIP 不算通过。 + +真实连接恢复测试无需产品数据库或长期证书,自动创建临时 PKI 和环回端口;从 `manager/backend` 执行: + +```bash +go test -count=10 -run '^TestRepositoryWorkerRecoversRealTRPCConnection$' \ + ./services/footstone/presentation +``` + +该项使用生产 Client、mTLS Listener 和 RemoteWorker,但任务/执行器是受控夹具,仅证明真实连接断开与同端口恢复,不冒充正式服务重部署或真实 RPM 发布。当前上游 tnet 的额外 race/checkptr 运行中止记录须保留;不能通过关闭 checkptr 宣称已通过。 + +### 12.2 专项证据与通过边界 + +| 项目 | 验证内容 | 当前证据范围 | +| --- | --- | --- | +| GPG Preview | 目标 RPM 数据库无 bash;首次/重复 Preview;错误元数据公钥;Preview 前后 RPM/公钥库摘要 | OC9.6 DNF 4.16.2/RPM 4.18.2,一次性容器,原生成脚本;没有在 VM 宿主删包 | +| 新 installroot | 产品 immutable 配置,双公钥,默认源禁用,真实安装 bash 及依赖 | 19 包实际安装、DNF/RPM 查询退出 0;不是只查询列表 | +| Retry/恢复 | 8 并发 Retry 仅一个在途子任务;租约、fencing、锁释放与同/跨仓并发 | 真实 OC9/MySQL,独立测试库 | +| 上传 | 同名 Session 隔离、Chunk/Resume;合法失败 Retry 成功导入;重复 Prepare/Confirm | 单测加真实 DB 事务;成功分支的包结果为夹具,不声称重新执行完整浏览器上传/GPG/repodata | +| 节点 | 旧心跳 OFFLINE 过滤、完整结果集 Summary、容量保留、心跳恢复 | 真实 DB,分页大小 1 时仍统计全部 3 个样本节点 | +| 命令助手 | 原参数/目标回填,切页后抽屉仍显示,生成命令预览 | 1440/1920 真实浏览器;未点击最终执行 | +| 磁盘 | 真实指标中光驱为 100%,KPI/趋势排除光驱;原始指标仍可查询 | 1440/1920 真实页面;真正数据盘满载保留及只有光驱的空态另由组件测试覆盖 | + +不得将上述定向结果替换历史 152 项结果,或把本地前端验证等同于全部修复已部署到现有后端。 + +## 13. 界面统一与已部署候选的联验 + +2026-10-23 在独立修复分支完成查询区统一,并实际替换测试环境的前端、Footstone 与 Worker。这是第 12 节源码专项之后的一次运行验证,不改写历史验收日期。2026-10-25 最新候选已更新至 Gitee `master` 和原 PR #420,PR 等待上游评审。[公开结果摘要](evidence/repository-style-validation-20261023.json)记录该轮任务、产物和证据摘要;[结构化报告 11.6](私有化软件仓库结构化验收报告.md#116-界面统一与候选产物上线回归)展示当前截图。 + +### 13.1 质量与浏览器检查 + +沿用第 12.1 节命令,Repository 测试为 **17 文件、109/109 PASS**(增加 4 项真实 TDesign 查询容器测试),相邻运维定向测试仍为 **42/42 PASS**。Repository ESLint、Stylelint、类型检查和正式 Vite build 通过;历史大 chunk 告警未隐藏。ESLint 使用项目既有 `.eslintrc` 配置模式,不把迁移全站 lint 配置混入本次改动。 + +在正式静态产物而非 Vite 上检查 1440×900、1920×1080 与 768px 宽度: + +1. 仓库、任务、节点、上游源及软件包采用一致查询区,查询/清空按钮可达;仓库、节点、上游源输入 Enter 及清空分别只触发一次请求。 +2. 仓库和任务表头与数据列横坐标误差为 0px;保留固定列、列设置与横向滚动。页面整体不发生横向溢出,宽表可在自身区域滚动。 +3. 节点“在线”、任务“手动/成功”均为内容宽度标签(样本 42px),不是整列背景;长 ID 可换行,状态色不被字段标签颜色覆盖。 +4. 软件包查询与批量业务按钮分区;SHA-256 短摘要单行显示,完整值可悬停查看。节点、任务 Drawer 和窄屏分发区不遮挡按钮。 + +### 13.2 实际产品回归 + +使用新建的专属测试 Repository 和无脚本副作用的已签名 RPM,不修改历史仓库,不清库、不新建 VM。实际通过: + +| 场景 | 实际验证 | +| --- | --- | +| 上传与恢复 | 7 个 Indexed Chunk 乱序、暂停/恢复、同片重传;文件长度及 SHA 一致 | +| 失败终态与合法 Retry | ENFORCE 缺公钥任务/Session 失败;登记正确公钥后重试原任务,真实发布新 Revision,Session COMPLETED | +| 同名文件 | 独立 Upload ID;相同内容再次上传返回 `NO_PACKAGE_CHANGES`,不覆盖旧 Session、不新增无意义 Revision;不是成功发布测试 | +| GPG/DNF | 包签名和元数据签名同时开启;首次 TMS Preview 为 READY,列出独立依赖;Confirm 实际安装;新 installroot 再次安装且 RPM 查询成功 | +| 同步与分发 | 从受控 immutable 小源同步;两个现有在线 Agent 均分发成功并回传校验值 | +| 导入与删除 | 显式签名 Key 映射后导入,关联全局 Task;依赖 Preview 只含绑定源;两个包一次删除,单 Task/新 Revision/两个结果项;错误 Digest 与旧 Revision 拒绝 | +| 统计与日志 | Node Summary 不随分页大小变化;任务统计不受状态筛选影响;真实下载后端日志 tar.gz,包含上下文、摘要、压缩日志与 SHA256SUMS | +| OCAI | 从仓库内失败任务 Drawer 确认上下文后真实流式分析,页面到达“分析完成”;建议仅供参考,不自动执行 | + +运行演示脚本前须检查测试工具;本次最小 Worker 镜像先因缺 `jq`、后因缺 `awk` 按设计退出,补齐测试容器工具后成功,未跳过预检或降低 GPG。一次性测试密钥不是正式环境密钥,不应复用为生产信任根。 + +### 13.3 边界 + +本节为已部署候选的界面与主要业务联验,不是完整 152 项重跑。aarch64 节点本次离线,未重复 Node RPM 安装/白名单竞态、全量容量/限速矩阵、重启重部署/destroy-restore 和 Case A/B/C 全矩阵;完整发布前仍须在可用、受控的拓扑按第 11 节逐项收集新 Evidence。此前上游 tnet race/checkptr 记录与既有 chunk 告警继续保留,不能把本节结果改成 `FINAL_READY=YES`。 + +## 14. 运行态故障与恢复实验 + +2026-10-24 在现有 OC9 管理面和仓库节点完成补验,结果见[恢复摘要](evidence/repository-recovery-validation-20261024.json)。本节补充第 13 节未执行的运行态取消、控制面短中断和 Worker 重启,不等于重新部署或 destroy/restore 全矩阵。 + +复现前先查询项目所有任务,确认没有其他 PENDING/RUNNING/CANCEL_REQUESTED;使用专属新仓与两个已签名、无脚本副作用的 fixture RPM,先正常同步建立活动 Revision。仅在测试仓的受控 HTTP 源上延迟 repomd 响应,不限速或阻断共享生产源。保留 GPG 策略、数据库、原任务历史、镜像及 Worker 备份。临时源的地址、端口和目录由操作者选择,不使用本文实验值作为产品默认。 + +| 实验 | 操作及观察 | 实际结果 | +| --- | --- | --- | +| 同步取消 | 等 RUNNING 且源确有延迟请求后取消;核对 Task、日志终态、活动 Revision 不变,再取消延迟并新建同步 | 修复前日志误记 FAILED 导致上报拒绝,租约回收收尾;修复后约 10.2 秒 CANCELED_BY_USER,后续同步成功 | +| 控制面短中断 | 专属同步在下载阶段时停止 Footstone 约 25 秒,finally 启动原容器;不重启 Worker | 相同 Worker PID 自动重连;原 Task attempt 1→2、fencing 6→7,发布成功 | +| Worker 重启 | 专属同步在下载阶段时由 systemd 重启 Worker;等待实际租约到期和维护扫描;解除专属源延迟 | 原 Task attempt 1→2、fencing 8→9,发布成功;不手工清锁或修改任务行 | + +新修复只调整 Worker 日志层对已取消 Context 的识别,新增底层返回进程错误和 nil 两种测试;Worker 包 `go test -count=1` 和 `go vet` 均通过。没有放宽状态转换、日志一致性或 fencing 校验。所有实验结束时无在途任务、节点 ONLINE,临时源进程已关闭,保留测试仓和证据。若要再次同步该仓,应重新启动自己的受控源或通过产品配置调整来源,不能将关闭的实验源当成产品故障。 + +本次未伪造旧 owner 的迟到提交来证明安全门禁,也未重跑 ARM、Node RPM 白名单竞态、完整 redeploy/destroy-restore;这些仍是后续门禁。 + +## 15. 当前候选补验记录 + +2026-10-24 继续在独立修复分支补验。第 13、14 节“未覆盖”保留原实验时点,后续增量以下表为准,不将旧 152 项 PASS 复制为当前候选结论。 + +| 范围 | 实际结果与边界 | +| --- | --- | +| Bootstrap | 真 MySQL 两种提交顺序连续 5 轮通过;原授权在白名单撤销/Task 终态后精确恢复同证书,不同 CSR/Agent/架构/授权 ID 拒绝;签发在事务外 | +| Migration | 空库安装、v17 六类旧记录升级 v18、默认值、8 外键、7 唯一索引、SQL 重入、结构等价通过;补验直接调用现有 `deploy.sh` 迁移函数自动发现并应用 v18;未修改冻结 v18,不冒称另有迁移 ledger | +| 真实配置 | Repository/Presentation 30 项、Application integration 14 项,0 Skip;使用隔离 MySQL/Redis/ClickHouse 和临时配置 | +| 恢复 | 当前候选隔离副本 restart/redeploy/destroy-restore 后摘要一致;TMS 7 服务重启后两台 Agent 实际任务成功;不销毁共享数据库 | +| DNF | Case A 同步仓、Case B 私有仓、Case C 跨仓依赖均真实安装成功;每组全新 OC9 容器、禁用默认源并核对来源 | +| 限速/并发 | 约 32 MiB:不限速约 5.36 秒;512 KiB/s、并发 2 时约 63.66 秒,速率误差约 0.6%,观测并发不超过 2;源不主动限速,同路径对照 | +| Mirror/Append | 上游两包变一包后,Mirror 发布一包、Append 保留两包;核对真实新 Revision 清单 | +| AUDIT | 两个未签名测试 RPM 仍发布;包清单保留 UNSIGNED、原因和状态提示;Node RPM 源仍采用 ENFORCE 与 Metadata Signing | +| 多目标安装 | 一个 Task 对两台 Agent 独立 Preview/Confirm,两个无脚本测试包安装成功,各 Target 保存独立结果 | +| Node RPM | 同一源码清单的 x86_64/aarch64 原生 RPM 真实签名,进入受保护标准安装源;两架构均由 TMS 从 immutable Revision 安装并以原 Node ID 自动注册、恢复 ONLINE | +| 节点多仓 | 两个真实 Node,ARM 同时承载多个仓库;从 Node Drawer 触发同步成功,查看全部任务带入当前 Node Filter | +| 调度 | 真实 MySQL 验证每日/每周、并发唯一 Run、运行中跳过、失败自动暂停与重新启用;产品页面/API 保存计划并展示下次时间,任务页面实际区分手动和定时调度;不冒称通过修改生产时钟跑过整个周期 | +| 多文件队列 | 实际 HTTP、并发上限 2、按字节进度、暂停及刷新重选同文件后只补缺片;两个文件均完成发布,切换新仓无旧队列;请求仅延迟转发,响应未 Mock | + +可复用测试入口,在 Repository Root 执行: + +```bash +# OC9 + Docker;仅创建独立测试容器,不连接部署数据库。 +bash scripts/tests/mysql_migration_upgrade_test.sh +# 验证签名恢复请求与多 override 契约,不重启实际 TMS。 +bash scripts/tests/repository_tms_recovery_auth_test.sh + +# 由私有环境注入 OCM_REPOSITORY_TEST_DSN,必须是独立 ocm_test_ 前缀库。 +# 会创建测试表,禁止指向生产库或打印 DSN。 +export OCM_REPOSITORY_TEST_ALLOW_SCHEMA=1 +cd manager/backend +go test -tags integration -count=5 -run '^TestMySQLBootstrapAuthorizationAndRevocation$' \ + ./services/footstone/domain/repositoryNodeService +``` + +多文件浏览器补验发现“单文件完成关闭整个 Drawer”和“跨仓复用旧队列”两个问题;修复后新静态产物已通过上述真实 HTTP 补验。组件卸载会暂停未完成项目并保存 Session,包括 Begin 延迟返回的情况;不会自动清理用户已有会话。页面刷新可能需要重选本地文件,不承诺浏览器能自动重新获得 File 对象。 + +Node RPM 版本选择使用同一当前源码另建旧版本号的测试包,明确不是恢复旧产品实现。安装候选只来自当前项目、架构及启用安装源;旧安装源的 Artifact 可以留作历史记录,但不能作为可安装候选。前端不单独实现 EVR 排序,实际安装仍由后端再次校验。当前补验完成旧版本号包的发布和实际页面选择,没有执行运行节点的降级安装。 + +[当前补验摘要](evidence/repository-final-validation-20261024.json)记录实际范围、产物与证据摘要哈希。当前配置下后端 53 个名称含 repository 的包及变更的 TMS 客户端共 529 项通过,Presentation 30 项、Application integration 14 项通过;默认跳过的 opt-in 用例另行显式补跑,不将 Skip 冒称执行。两个旧 Worker 夹具补齐现有必需的主节点绑定后,四项 MySQL/GPG/DNF/同步集成测试全部通过,未放宽产品节点门禁。Repository 前端更新至 20 文件、126 项通过,局部 ESLint/Stylelint、Repository 类型检查及正式构建通过;全站历史类型诊断仍另列,不冒称全站零错误。 + +最终状态以机器结果及完整发布门禁为准,不能把校验器正负向测试使用的人工 PASS fixture 当成产品 152 项 PASS。 + +补验早期的[152 项进度清单](evidence/repository-acceptance-progress-20261024.json)为 144 PASS、3 NOT_STARTED、5 BLOCKED;严格校验退出码为 1,不是最终通过。剩余非 PASS ID:`127`、`133`、`135`、`137`、`138`、`145`、`148`、`TEACHER-NODE-01`。此前诊断意外输出的外部 OCAI 密钥已由用户撤销并替换;[外部轮换阶段](evidence/repository-credential-rotation-20261024.json)验证供应方 HTTP 200 及真实任务分析,只重建 OCAI。随后另行完成 HMAC/JWT 和 MySQL/Redis/ClickHouse 内部凭据轮换,见[内部认证摘要](evidence/repository-internal-authentication-rotation-20261024.json)及 15.2。两阶段保留各自实际范围,不将早期 `PENDING` 当作最新状态,也不把轮换通过等同于完整发布通过。 + +这一补验时点的完整 152 项结果、最终文档检查和 Makefile 总入口尚未全部完成,当时为 `FINAL_READY=NO`。该记录保留其原始边界,最终完整结果另见第 16 节。 + +### 15.1 独立容器 MySQL 模式 + +`repository-report.sh`、`repository-e2e.sh`、`repository-diagnosis.sh` 和 `repository-gpg.sh` 共用 `scripts/repository-test-mysql.sh`。不设置下面四项变量时,仍按原来的本机 socket/MariaDB 模式使用 sudo;显式设置时只允许操作经过身份核验的独占测试 MySQL,不启动、停止或连接宿主 MariaDB。 + +| 参数 | 作用与前提 | +| --- | --- | +| `OCM_REPOSITORY_TEST_MYSQL_HOST` | 固定为 IPv4 loopback `127.0.0.1`,不是部署数据库地址 | +| `OCM_REPOSITORY_TEST_MYSQL_PORT` | 部署人选择的空闲 10000~65535 高端口;不能使用宿主 3306 | +| `OCM_REPOSITORY_TEST_MYSQL_PASSWORD_FILE` | 私有常规文件,禁止 symlink,权限 0600/0400;只保存本次独占测试容器的随机十六进制密码,不入日志或 Git | +| `OCM_REPOSITORY_TEST_MYSQL_CONTAINER` | 本次创建的 `ocm-repo-test-mysql-*` 容器名;带 `io.ocmanager.repository.acceptance.mysql=true` 标签,host network 并显式绑定相同 loopback/port | +| `OCM_REPOSITORY_TEST_MYSQL_IMAGE` | 下例使用的同版 MySQL 镜像,可改为自己可达的镜像地址;不是 helper 的隐式数据库选择参数 | + +前提为 OC9、可管理 Docker 的账号、`mysql` 客户端、`openssl`、`jq`、`stat`、`ss`、Bash 和可达的 MySQL 8.0.35 镜像。所有命令从 Repository Root 执行。以下示例放在**单独 Shell**,创建自己的临时容器、真实鉴权查询并验证 server UUID,不读取部署凭据;结束时只清理自己创建且标签匹配的容器和临时密码文件: + +```bash +set -euo pipefail +test -f scripts/repository-test-mysql.sh +for tool in docker mysql openssl jq stat ss; do command -v "$tool" >/dev/null; done +read -r -p '请输入本次独占测试 MySQL 的空闲高端口(10000~65535): ' test_port +[[ $test_port =~ ^[1-9][0-9]{4}$ ]] && ((test_port >= 10000 && test_port <= 65535)) +test -z "$(ss -H -ltn "sport = :${test_port}")" +umask 077 +test_private_dir=$(mktemp -d) +test_container="ocm-repo-test-mysql-$(openssl rand -hex 6)" +test_container_created=0 +cleanup_test_mysql() { + if [[ $test_container_created == 1 ]] && + [[ $(docker inspect --format '{{index .Config.Labels "io.ocmanager.repository.acceptance.mysql"}}' "$test_container" 2>/dev/null) == true ]]; then + docker rm -fv "$test_container" >/dev/null + fi + rm -f -- "${test_private_dir}/password" "${test_private_dir}/mysql.env" + rmdir -- "$test_private_dir" +} +trap cleanup_test_mysql EXIT +openssl rand -hex 32 >"${test_private_dir}/password" +printf 'MYSQL_ROOT_PASSWORD=%s\n' "$(<"${test_private_dir}/password")" >"${test_private_dir}/mysql.env" +chmod 0600 "${test_private_dir}/password" "${test_private_dir}/mysql.env" +docker run -d --name "$test_container" --network host \ + --label io.ocmanager.repository.acceptance.mysql=true \ + --env-file "${test_private_dir}/mysql.env" \ + "${OCM_REPOSITORY_TEST_MYSQL_IMAGE:-mysql:8.0.35}" \ + "--port=${test_port}" --bind-address=127.0.0.1 >/dev/null +test_container_created=1 +test_mysql_ready=0 +for attempt in $(seq 1 120); do + if MYSQL_PWD="$(<"${test_private_dir}/password")" mysql \ + --protocol=TCP --host=127.0.0.1 --port="$test_port" --user=root \ + --connect-timeout=2 -Nse 'SELECT 1' >/dev/null 2>&1; then + test_mysql_ready=1; break + fi + sleep 1 +done +test "$test_mysql_ready" == 1 +export OCM_REPOSITORY_TEST_MYSQL_HOST=127.0.0.1 +export OCM_REPOSITORY_TEST_MYSQL_PORT="$test_port" +export OCM_REPOSITORY_TEST_MYSQL_PASSWORD_FILE="${test_private_dir}/password" +export OCM_REPOSITORY_TEST_MYSQL_CONTAINER="$test_container" +source scripts/repository-test-mysql.sh +repository_test_mysql_configure ocm_repository_preflight ocm_preflight +repository_test_mysql_admin -Nse 'SELECT 1' +# 在此 Shell 中执行需要的 report/e2e/diagnosis/gpg 脚本。 +# 全量入口另需当前152项结果、真实产品API/TMS参数和恢复窗口,不由本例自动提供。 +``` + +helper 拒绝缺参数、错误标签、宿主 3306、非 loopback、宽松密码文件和 TCP UUID 不匹配的配置。它只接收 `ocm_repository_*` 独占库名和 `ocm_*` 测试账号;四个脚本自动生成自己的临时库和账号,并将 Go DSN 指向同一已验证端点。Presentation/Application 测试仍须按 1.2 准备独立真实配置及 Redis/ClickHouse 等依赖,不能复制部署的 Secret,也不能用 helper 掩盖缺少运行前提。 + +`bash scripts/tests/repository_test_mysql_contract_test.sh` 验证参数、路由、文件权限、UUID 和失败传播契约,使用受控命令替身,不等于真实 MySQL 集成。当前 OC9 另行运行了真实容器验证:TCP/admin/user/Go DSN 端点一致、建表查询和清理成功,宿主 MariaDB 状态不变。完整验收还必须实际执行四个业务测试脚本,不能仅凭契约测试通过。 + +### 15.2 认证轮换后的产品复查 + +[内部认证摘要](evidence/repository-internal-authentication-rotation-20261024.json)记录新 HMAC 签名 200、旧 HMAC 签名 401、STRICT 保持;JWT 运行值已替换,旧浏览器 Token 请求失败,但返回未明确标为 signature error,因此不声称验证了特定底层错误原因。MySQL root/应用账号、Redis、ClickHouse 的旧凭据均被实际服务拒绝,新凭据鉴权及 ClickHouse Distributed 查询成功。Redis 临时持久化标记和 Repository/Task/Revision 指纹保持;镜像、挂载和卷没有因轮换改变。ZooKeeper 当前未启用认证,本次没有增加 SASL,也不把未使用的密码变量当作认证。 + +轮换后重新退出旧会话并真实登录成功;管理 API 可查询 x86_64 与 aarch64 Node,两者当前为 ONLINE、版本 `0.2.2-0.review20261024.1.oc9`。对已有失败任务再次构建 OCAI 上下文并完成真实分析,分析前后 Task 数均为 128,没有由分析新增 Repository 变更任务;同时可从全局 Task 查询已经成功的 `INVENTORY_IMPORT`。公开脱敏记录见[轮换后产品复查](evidence/repository-post-rotation-product-validation-20261024.json)。不从分析完成后已关闭 Dialog 的文字检测推断变更是否执行,原始检测结果保留。 + +### 15.3 已部署错误目录检查 + +[Node RPM 错误映射摘要](evidence/repository-node-rpm-error-validation-20261024.json)绑定当前 7 文件 SHA、16 项 Catalog/HTTP 映射测试、vet/build 和实际 Footstone v2 产物。产品 API 实测:系统安装源同步被 `REPOSITORY_NODE_SYNC_FORBIDDEN` 拒绝;显式确认后删除被 `REPOSITORY_SYSTEM_MANAGED` 拒绝;缺失或架构不匹配 Artifact 被 `REPOSITORY_NODE_ARTIFACT_NOT_FOUND` 拒绝。对应响应保留英文 Code、中文原因、中文建议和技术原因,Task 数 128→128,没有通过失败请求创建新任务。 + +遗漏 `confirm_delete` 的普通参数错误仍是 `InternalError` 英文提示;这是明确保留的边界,不能把此次专项写成所有参数错误均已中文化。这一错误目录专项复查时点的完整入口仍在执行,当时没有把专项或服务健康合并为完整 PASS;最终结果另见第 16 节。 + +## 16. 当前候选最终完整验收 + +2026-10-24 在 OpenCloudOS 9 对冻结候选实际执行 `make -C manager repo-acceptance`,退出码为 0;核心/增强矩阵 31/31、Evidence Bundle 9/9、全部 11 个执行步骤及严格 152 项校验均通过。预期、唯一预期、实际、唯一实际均为 152,重复、缺失、意外、非 PASS 和不完整证据条目均为 0。结果见[最终候选摘要](evidence/repository-final-validation-20261024.json)、[152 项机器结果](evidence/repository-acceptance-progress-20261024.json)和[完整发布验收](evidence/repository-release-validation-20261024.json)。 + +```text +152/152 PASS +OVERALL_ACCEPTANCE = PASS +FINAL_READY = YES +``` + +全量运行发现的 GPG 临时密钥用例使用了过长 Unix socket 路径,已只在测试夹具采用受控短临时目录并完整重跑;隔离运行器同步固定工具路径和最小环境注入。此收口没有修改 Repository 业务链路或放松 GPG/mTLS/Mutation Lock。源码清单、锁文件和运行产物指纹与证据绑定,P14 只更新文档与证据链接。 + +| 最终门禁 | 实际结果 | +| --- | --- | +| 完整 make 入口 | exit code = 0 | +| 11 个真实执行步骤 | 全部 exit code = 0 | +| 核心/增强矩阵 | 31/31 PASS | +| Evidence Bundle | 9/9 PASS | +| 权威 Criteria / 实际结果 | 152 / 152,唯一性完整,0 异常 | +| restart / redeploy / destroy-restore / TMS restart | PASS | +| P14 文档与非文档源码 SHA-256 一致性 | PASS | + +复现须使用自己当前的源码快照、独立运行配置和测试数据库执行第 1、15.1 节及正式 Makefile 入口,不能把本次公开 PASS JSON 当作新环境的结果输入。严格校验器只验证提交给它的结构化结果,真实操作、日志和产物一致性仍需逐项提供。 + +2026-10-25 文档更新增加用户手册并重整三份说明文档的开篇。业务代码未变化;新增手册的相对链接、25 张截图文件及 SHA-256 与截图清单的对应关系已逐项检查通过。2026-10-24 的 152 项结果仍绑定其原候选与运行证据。 + +验收范围继续保持明确:ZooKeeper 的历史部署未启用认证,本轮未扩展 SASL;Repository 定向类型检查通过不等于全站历史 TypeScript 问题已清零;旧版本号 RPM 已验证页面选择与 EVR 策略,不声称对运行节点执行过历史代码降级;TMS Agent 需先按既有 TMS 部署方式安装并在线,本轮自动安装的是 Repository Node RPM,不是为新机器自动引导部署 TMS。遗漏删除确认的普通参数错误仍可能返回英文技术提示,不能据此宣称所有参数校验均已中文化。 diff --git "a/docs/\347\247\201\346\234\211\345\214\226\350\275\257\344\273\266\344\273\223\345\272\223\347\224\250\346\210\267\346\211\213\345\206\214.md" "b/docs/\347\247\201\346\234\211\345\214\226\350\275\257\344\273\266\344\273\223\345\272\223\347\224\250\346\210\267\346\211\213\345\206\214.md" new file mode 100644 index 0000000000000000000000000000000000000000..cbae0f7634e48f9cc8975e5ff4ba2aa9fe85a47a --- /dev/null +++ "b/docs/\347\247\201\346\234\211\345\214\226\350\275\257\344\273\266\344\273\223\345\272\223\347\224\250\346\210\267\346\211\213\345\206\214.md" @@ -0,0 +1,345 @@ +# OCManager 私有化软件仓库用户手册 + +| 项目 | 内容 | +| --- | --- | +| 文档版本 | V1.0 | +| 更新日期 | 2026-10-25 | +| 适用读者 | 仓库管理员、软件发布人员、节点运维人员和软件使用人员 | +| 产品基线 | Repository 前端与后端代码功能基线 `ddbdfea457644ed7d01e6c209e9dfd2168cd6953` | +| 配套文档 | [方案设计](私有化软件仓库方案设计.md) · [部署说明](私有化软件仓库部署说明.md) · [测试验收](私有化软件仓库测试验收.md) · [结构化验收报告](私有化软件仓库结构化验收报告.md) | + +## 文档说明 + +### 产品背景 + +企业内部同时使用发行版软件包和自研 RPM 时,需要明确软件包从哪里来、经过哪些检查、发布到哪个仓库,以及目标节点安装的是哪个版本。分散存放和手工维护仓库元数据会增加包错放、签名漏检、版本不一致和故障难定位的风险。 + +OCManager 将上游同步、私有包上传、RPM/GPG 校验、隔离项处理、Revision 发布、节点分发、任务追踪和客户端配置集中到控制台。用户可基于标准 YUM/DNF 仓库向 OpenCloudOS 9 节点安装软件包,并通过不可变 Revision、任务结果和签名信息核对发布内容。 + +### 手册范围与阅读方式 + +本手册面向仓库管理员、软件发布人员、节点运维人员和软件使用人员,聚焦日常控制台操作。第一次发布软件包可按“选择项目 → 配置来源 → 创建仓库 → 同步或上传 → 检查任务与包 → 发布 Revision → 分发或生成客户端配置”阅读第 1–10 节;任务诊断、OCAI、清单导入导出和常见问题见第 11–15 节。部署命令和自动化复现分别见[部署说明](私有化软件仓库部署说明.md)与[测试验收](私有化软件仓库测试验收.md)。 + +页面名称以当前产品为准。截图来自 2026-10-24 的实际控制台,节点地址、仓库地址和身份信息已遮蔽;界面数据随环境变化。部署与验收命令见配套文档,本手册聚焦日常控制台操作。 + +## 目录 + +**准备和建仓** + +- [1. 功能入口与操作对象](#1-功能入口与操作对象) +- [2. 建仓到客户端安装](#2-建仓到客户端安装) +- [3. 配置上游源](#3-配置上游源) +- [4. 创建软件仓库](#4-创建软件仓库) + - [4.1 选择同步模式](#41-选择同步模式) +- [5. 查看仓库和 Revision](#5-查看仓库和-revision) + +**软件包与节点运维** + +- [6. 上传与管理软件包](#6-上传与管理软件包) + - [6.1 安装软件包到节点](#61-安装软件包到节点) + - [6.2 批量删除软件包](#62-批量删除软件包) +- [7. 查询任务和处理失败](#7-查询任务和处理失败) +- [8. 管理仓库节点](#8-管理仓库节点) +- [9. 分发不可变 Revision](#9-分发不可变-revision) + +**客户端、安全与日常管理** + +- [10. 生成客户端仓库配置与 GPG 校验](#10-生成客户端仓库配置与-gpg-校验) +- [11. 使用 OCAI 分析任务](#11-使用-ocai-分析任务) +- [12. 导入和导出仓库定义](#12-导入和导出仓库定义) +- [13. 常见情况处理](#13-常见情况处理) +- [14. 日常操作建议](#14-日常操作建议) +- [15. 相关文档](#15-相关文档) + +## 1. 功能入口与操作对象 + +左侧“软件仓库”菜单包含四个主要入口: + +| 页面 | 路径 | 主要用途 | +| --- | --- | --- | +| 仓库管理 | `/repository/list` | 创建仓库、筛选、同步、导入导出仓库定义 | +| 任务总览 | `/repository/tasks` | 查询同步、上传、删除、安装、分发和导入等任务 | +| 仓库节点 | `/repository/nodes` | 查看仓库节点容量、仓库占用、最近任务及安装状态 | +| 上游源 | `/repository/sources` | 管理可在建仓时选择的上游来源配置 | + +控制台操作受当前项目和账号权限约束。进行建仓、导入或跨节点安装前,先确认页面当前项目。仓库节点负责保存 RPM 和 repodata;TMS Agent 是安装或接收分发任务的目标。两者用途不同,节点选择时应核对页面字段和目标架构。 + +## 2. 建仓到客户端安装 + +```text +选择项目 + → 选择或配置上游源 + → 创建基线仓/私有仓 + → 同步上游或上传软件包 + → 检查任务、软件包和隔离项 + → 发布 immutable Revision + → 分发 Revision 或生成客户端 .repo 配置 + → 在 OpenCloudOS 9 客户端查询并安装软件包 +``` + +先在“仓库节点”确认至少有一个在线且容量满足预期的存储节点。同步和上传任务均写入所选仓库节点;Revision 发布后,可供只读 HTTP 客户端访问。 + +## 3. 配置上游源 + +打开“软件仓库 → 上游源”,可以使用产品内置的 OpenCloudOS BaseOS/AppStream 来源,也可以添加项目自己的来源。新增配置时填写来源名称、系统/产品、通道、地址模板和支持的版本、架构。模板中的版本与架构变量按表单提示填写;保存后可在建仓表单中选择。 + +内置来源由产品维护,建仓时根据所选系统版本和架构生成上游地址。自定义地址适用于组织镜像或经过批准的内部源。上游是否包含所需架构和软件包由源端决定;表单只提供该来源支持的选项。 + +![上游源列表](images/repository/repository-current-20261024-sources.png) + +*图 1:上游源列表。内置来源与项目自定义来源在同一页面管理。* + +![新增上游源](images/repository/repository-current-20261024-source-form.png) + +*图 2:上游源配置表单。地址模板和适用范围按实际上游填写。* + +## 4. 创建软件仓库 + +1. 打开“仓库管理”,选择“创建仓库”。 +2. 填写仓库名称、系统/产品、版本、仓库类型和架构。 +3. 选择保存 RPM 与 repodata 的主仓库节点。 +4. 选择上游源并检查生成的上游地址。私有仓库可不配置上游,按页面设置上传软件包。 +5. 设置同步模式、并发下载、下载限速和 GPG 策略。启用 Metadata Signing 时选择节点已有的签名 Key。 +6. 配置 Include/Exclude 过滤范围,检查后创建仓库。 + +![选择上游创建仓库](images/repository/repository-current-20261024-create-upstream.png) + +*图 3:建仓表单可按上游来源生成地址,并设置类型、架构、仓库节点与同步策略。* + +![配置同步筛选](images/repository/repository-current-20261024-create-filter.png) + +*图 4:按来源支持的包组和包名配置筛选范围。* + +Include 为空时表示按配置同步上游全部匹配内容。发行版仓库可能较大,应先确认节点容量和任务预算;可优先选择上游提供的常用包组,或填写准确的软件包名称。执行前端会调用后端预检,未通过时应根据提示调整筛选或容量配置。 + +### 4.1 选择同步模式 + +| 模式 | 行为 | +| --- | --- | +| 镜像(MIRROR) | 仓库内容跟随上游;上游已删除的软件包会从后续发布内容中移除 | +| 追加(APPEND) | 保留当前 Revision 中的既有软件包,并加入本次从上游取得的包 | + +已经创建的仓库可在仓库列表或仓库详情中启动手动同步。节点详情里的“同步”操作调用相同的 Repository Sync 链路。已有同步任务时页面显示“同步中”。 + +## 5. 查看仓库和 Revision + +“仓库管理”列表支持按系统/产品、版本、类型、架构和关键字筛选。点击仓库名称进入详情。列表展示包数量、内容大小、状态、存储节点和最近同步结果;操作列可打开详情、编辑、同步或删除。 + +![仓库管理列表](images/repository/repository-current-20261024-list.png) + +*图 5:仓库列表。筛选条件、核心指标和行操作按项目范围显示。* + +仓库详情包含“概览”“软件包”“任务”“节点分发”“仓库配置”五个页签: + +- **概览**:查看发布状态、当前 Revision、软件包数量、当前内容大小和隔离项数量。 +- **软件包**:检索 NEVRA、签名、SHA-256、来源、Revision 和入库时间;上传、安装或批量删除也从该页进入。 +- **任务**:查看当前仓库的同步、上传和变更任务。 +- **节点分发**:创建不可变 Revision 分发并跟踪各目标结果。 +- **仓库配置**:复制或下载客户端 `.repo` 配置,并检查 GPG 公钥和策略。 + +![仓库概览](images/repository/repository-current-20261024-overview.png) + +*图 6:仓库概览显示当前发布 Revision 与包数、内容大小等指标。* + +“当前内容大小”统计 active Revision 中 RPM 的逻辑大小;“节点实际占用”包含该仓库在节点上 Revision、暂存区、隔离区、元数据等实际文件占用。查看节点磁盘容量时应以仓库节点页面的文件系统统计为准。 + +## 6. 上传与管理软件包 + +在仓库详情的“软件包”页点击“上传软件包”,选择 RPM 或支持的归档文件后加入上传队列。可一次选择多个文件;队列显示各文件状态、已传字节数、总进度和并发数。上传完成后,系统校验内容并按仓库策略进入可用 Revision 或隔离区。 + +![软件包列表](images/repository/repository-current-20261024-packages.png) + +*图 7:软件包清单展示架构、签名状态、摘要、来源与所属 Revision。* + +![上传队列](images/repository/repository-current-20261024-upload-queue.png) + +*图 8:上传队列入口。选择文件后,队列会显示逐文件与总体字节进度。* + +网络中断或手动暂停后,可恢复同一 Upload Session 并只补传缺失分片。若刷新页面后浏览器无法重新访问原文件,重新选择相同的本地文件即可匹配已有 Session;浏览器不会自动恢复本地文件句柄。 + +RPM 校验失败或被策略拒绝的软件包会显示在“隔离区”。打开隔离项可检查 NEVRA、拒绝原因、文件大小和入库时间。修正上游包、架构或签名问题后重新上传合规包;隔离项的处置不会将失败文件发布到可用仓库。 + +![隔离区](images/repository/repository-current-20261024-quarantine.png) + +*图 9:隔离区用于查看被拒绝的文件和校验原因。* + +### 6.1 安装软件包到节点 + +1. 在软件包清单勾选目标包,点击“安装到节点”。 +2. 选择一个或多个在线 TMS Agent。 +3. 点击“预览安装”,查看各节点的 selected packages、依赖包、升级/替换项、下载量、磁盘变化和状态。 +4. 检查所有目标;只有状态为 READY 的目标可确认执行。 +5. 确认后到任务总览查看每个目标的执行结果。 + +![选择安装节点](images/repository/repository-current-20261024-package-install-nodes.png) + +*图 10:为同一安装任务选择多个目标 Agent。* + +![安装预览](images/repository/repository-current-20261024-package-install-preview.png) + +*图 11:确认前查看目标节点的 DNF 事务预览。* + +执行结果按目标分别记录。失败目标会优先展示,详情保留错误码和日志摘要;需要进一步分析时可从任务详情启动 OCAI 分析。 + +![软件包安装任务结果](images/repository/repository-current-20261024-package-install-result.png) + +*图 12:安装任务详情展示每个目标 Agent 的执行状态,并提供脱敏任务日志包下载。* + +### 6.2 批量删除软件包 + +勾选需要删除的软件包,选择“批量删除”。预览会显示直接影响和传递依赖影响,默认不勾选受影响的依赖包。确认后系统创建一个删除任务,并在任务完成后发布一个新 Revision。检查预览清单后再确认。 + +![批量删除预览](images/repository/repository-current-20261024-batch-delete-preview.png) + +*图 13:批量删除预览展示依赖影响和待删除内容。* + +任务结束后,在结果详情检查每个 NEVRA 的处理状态,以及新 Revision 是否发布成功。 + +![批量删除任务结果](images/repository/repository-current-20261024-batch-delete-result.png) + +*图 14:一个删除任务包含逐包结果和发布版本信息。* + +## 7. 查询任务和处理失败 + +打开“任务总览”查看项目内的软件仓库任务。顶部统计包含等待、执行中、失败和已完成数量;筛选区支持仓库、节点、任务类型、触发方式、状态及创建时间。点击统计卡片可快速设置状态筛选。 + +![任务总览](images/repository/repository-current-20261024-tasks.png) + +*图 13:任务列表和状态统计。任务时间筛选按创建时间执行。* + +点击任务行可打开任务详情,查看任务 ID、阶段、进度、开始/结束时间、目标结果和日志。可下载完整任务日志包;允许取消或重试的任务会显示对应操作。重试会创建新的任务记录,历史任务保留供追踪。 + +遇到失败时,先记录英文 Error Code,再阅读中文原因和建议;随后查看日志中的命令阶段和错误输出。仅在配置、输入或外部依赖修正后重试,避免重复提交相同失败任务。 + +![任务详情与失败信息](images/repository/repository-current-20261024-task-failure.png) + +*图 15:任务详情提供错误码、失败摘要和日志查看入口。* + +## 8. 管理仓库节点 + +仓库节点页面展示节点名称、架构、在线状态、节点组件版本、仓库数量、实际占用、文件系统容量、最近心跳及最近错误。节点详情 Drawer 提供节点存储概览、其承载的 Repository/Source 列表和最近任务。每行仓库可直接查看;满足状态条件时可启动同步。 + +![仓库节点列表](images/repository/repository-current-20261024-nodes.png) + +*图 16:节点列表展示后端汇总的在线状态和容量数据。* + +![仓库节点运维 Drawer](images/repository/repository-current-20261024-node-drawer.png) + +*图 17:节点详情可查看仓库占用、Revision、同步状态和最近任务。* + +“手动注册”用于登记已经安装并启动 Repository Node 服务的节点。“安装仓库节点”通过 TMS 向在线 Agent 下发安装/升级任务,适用于自动安装场景。安装入口包含三个页签: + +1. **安装节点**:选择在线 Agent、架构、Node RPM 版本、节点名称及控制/发布地址,提交 TMS 安装任务。 +2. **Node RPM 安装源**:按架构指定系统管理的标准 YUM/DNF 仓库,并从该仓库当前不可变 Revision 登记已发布的 Node RPM Artifact。 +3. **注册白名单**:按项目和 TMS Agent ID 授权注册。架构与 IP 可作为附加约束;IP 本身不构成节点身份。 + +安装时默认选择该架构当前安装源中的最新 RPM EVR 版本,管理员也可选择已发布的历史版本。目标节点通过临时仓库配置从不可变 Revision 下载 RPM;注册授权通过后自动登记为仓库节点。安装任务及后续心跳可在任务总览和仓库节点页面检查。 + +![Node RPM 安装源与 Artifact](images/repository/repository-current-20261024-node-install-sources.png) + +*图 18:按架构启用 Node RPM 安装源,并登记当前 Revision 中的 Artifact。* + +![注册白名单](images/repository/repository-current-20261024-node-allowlist.png) + +*图 19:白名单按 TMS Agent 身份授权,可附加架构和 IP 限制。* + +![安装或升级仓库节点](images/repository/repository-current-20261024-node-install.png) + +*图 20:选择目标 Agent 与 Node RPM 版本后,由 TMS 发起安装任务。* + +## 9. 分发不可变 Revision + +在仓库详情的“节点分发”页选择 Revision,点击“创建分发”,选择一个或多个在线 TMS Agent 并提交。系统将固定 Revision 内容分发到各目标,校验 manifest 和 repomd 摘要,并逐目标记录结果。分发目标来自 TMS Agent 列表;新增目标须先接入并在线。 + +![Revision 分发列表](images/repository/repository-current-20261024-distribution.png) + +*图 21:分发记录按 Revision 汇总各目标结果。* + +![Revision 分发详情](images/repository/repository-current-20261024-distribution-detail.png) + +*图 22:分发详情显示目标状态、校验摘要和执行输出。* + +分发结果为部分成功时,可在详情中定位失败目标及输出。确认网络、目标磁盘和 Agent 状态后,可按页面允许的重试操作处理失败目标。 + +## 10. 生成客户端仓库配置与 GPG 校验 + +打开仓库详情的“仓库配置”页,选择当前发布版本或指定 immutable Revision,复制或下载产品生成的 `.repo` 文件。配置会包含对应的 Base URL、启用状态和实际发布策略中的 `gpgcheck`、`repo_gpgcheck` 与 `gpgkey`。 + +![客户端 Repo 配置](images/repository/repository-current-20261024-config.png) + +*图 23:产品按发布版本生成客户端配置,支持复制和下载。* + +`.repo` 文件应保存到目标 OpenCloudOS 9 客户端的 `/etc/yum.repos.d/`,再使用 DNF/YUM 验证。以下命令中的文件名、仓库 ID 和包名替换为页面生成的值: + +```bash +sudo install -m 0644 /etc/yum.repos.d/.repo +sudo dnf clean metadata +sudo dnf --disablerepo='*' --enablerepo='' makecache +sudo dnf --disablerepo='*' --enablerepo='' install +``` + +客户端网络必须能够访问 Base URL 和页面列出的公钥地址。开启包签名或 Metadata Signing 后,按配置启用相应校验;不要通过关闭校验来掩盖公钥未发布或签名不匹配的问题。 + +在“GPG 公钥与策略”页查看 RPM 公钥、仓库签名策略和 Metadata Signing Key 状态。Metadata 私钥由仓库节点保管。启用签名策略前,先确保所需公钥可访问,并在目标节点完成公钥同步。 + +![GPG 公钥与签名策略](images/repository/repository-current-20261024-gpg.png) + +*图 24:检查包签名策略、公钥与 Metadata Signing 配置。* + +## 11. 使用 OCAI 分析任务 + +在仓库任务详情打开 OCAI 分析,确认页面给出的任务上下文和脱敏范围,然后发起分析。结果按故障分类、可能原因、日志证据和建议步骤展示。分析记录可查看历史会话。 + +OCAI 只读取脱敏任务上下文并生成建议,不会代替操作人员修改仓库、取消任务或安装软件包。执行变更前应核对建议与任务日志、仓库配置和节点状态。 + +![OCAI 任务分析结果](images/repository/repository-current-20261024-ocai-result.png) + +*图 25:OCAI 将任务错误、原因依据与处理建议放在分析结果中。* + +## 12. 导入和导出仓库定义 + +仓库管理页的导出入口生成当前项目的 Repository Inventory JSON。导出内容用于迁移仓库定义和配置,不包含 RPM 文件、Revision 内容、私钥、Secret 或任务历史。 + +导入时选择 Inventory 文件、目标仓库节点和冲突处理策略;若清单引用 Metadata Signing Key,需要映射到目标环境已有的 `key_ref`。点击“预览导入”检查每一项的 CREATE、SKIP、CONFLICT 或 INVALID 结果;只有预览状态允许确认时才提交导入任务。 + +![Inventory 导入预览](images/repository/repository-current-20261024-inventory-import-preview.png) + +*图 25:确认前检查目标节点、冲突策略、签名 Key 映射和逐项结果。* + +导入结果可在全局任务总览中查看。导入不会恢复 RPM、Revision 文件、签名私钥或历史任务;这些内容需通过正常仓库发布、节点安装和备份流程分别准备。 + +![Inventory 导入任务](images/repository/repository-current-20261024-inventory-import-task.png) + +*图 26:Inventory 确认后作为标准仓库任务记录,可在任务详情查看结果。* + +## 13. 常见情况处理 + +| 现象 | 检查方法 | +| --- | --- | +| 建仓时没有可选上游 | 检查项目、系统版本和架构;在“上游源”页确认来源已启用并支持该组合 | +| 同步前容量预检未通过 | 检查节点可用空间、inode、文件系统和当前仓库占用;缩小 Include 范围或扩充存储 | +| 上传后软件包进入隔离区 | 查看拒绝原因、NEVRA、架构和签名要求;修正包后重新上传 | +| DNF 找不到包或 repodata | 检查任务是否成功生成 Revision,客户端使用的 Base URL 是否与配置页一致 | +| DNF 签名校验失败 | 检查 `gpgkey` 可达性、RPM 签名公钥、`gpgcheck` 与 Metadata Signing 配置 | +| Node RPM 安装被拒绝 | 检查目标 Agent 在线状态、架构、当前安装源、Artifact 发布状态和白名单授权 | +| 分发失败或部分成功 | 在分发详情中查看失败目标、Agent 状态、磁盘和 manifest 校验输出 | +| OCAI 没有返回分析结果 | 检查 OCAI 服务状态和任务上下文生成结果;规则诊断与任务日志仍可用于排查 | +| 任务长时间没有推进 | 打开任务详情检查当前阶段和最近日志;根据任务类型确认节点、Worker 和外部源可用后再使用重试操作 | + +## 14. 日常操作建议 + +1. 建仓前检查项目、架构、上游地址与主仓库节点。 +2. 对大型上游先配置 Include 规则,查看容量预检再提交同步。 +3. 上传后检查任务结果、软件包签名和隔离区状态。 +4. 在仓库概览确认新的 active Revision 与包数量,再分发或下载 `.repo` 配置。 +5. 在目标 OpenCloudOS 9 客户端只启用待验证仓库,执行真实 DNF/YUM 查询和安装。 +6. 保留任务 ID、Revision ID、客户端日志和 SHA-256 等证据,便于后续排查。 +7. 变更仓库定义前核对正在运行的任务;删除仓库前按页面提示处理已发布包、公钥和任务保护条件。 + +## 15. 相关文档 + +- [方案设计](私有化软件仓库方案设计.md):模块边界、数据模型、API、安全和发布设计。 +- [部署说明](私有化软件仓库部署说明.md):OpenCloudOS 9 环境要求、部署脚本、节点准备和恢复步骤。 +- [测试验收](私有化软件仓库测试验收.md):单项复现入口、完整验收命令、通过标准和证据定位。 +- [结构化验收报告](私有化软件仓库结构化验收报告.md):验收项目、实现/测试证据和页面操作示例。 +- [页面截图清单](evidence/repository-documentation-screenshots-20261024.json):当前控制台截图的尺寸、SHA-256 和隐私处理信息。 diff --git "a/docs/\347\247\201\346\234\211\345\214\226\350\275\257\344\273\266\344\273\223\345\272\223\347\273\223\346\236\204\345\214\226\351\252\214\346\224\266\346\212\245\345\221\212.md" "b/docs/\347\247\201\346\234\211\345\214\226\350\275\257\344\273\266\344\273\223\345\272\223\347\273\223\346\236\204\345\214\226\351\252\214\346\224\266\346\212\245\345\221\212.md" new file mode 100644 index 0000000000000000000000000000000000000000..0507cf4d003ca7b63da48dffbc1ad840cbda9714 --- /dev/null +++ "b/docs/\347\247\201\346\234\211\345\214\226\350\275\257\344\273\266\344\273\223\345\272\223\347\273\223\346\236\204\345\214\226\351\252\214\346\224\266\346\212\245\345\221\212.md" @@ -0,0 +1,1808 @@ +# OCManager 私有化软件仓库结构化验收报告 + +> 发布隐私说明:页面图中的测试地址和相关节点标识已遮蔽,灰色区域不代表产品空值;实际部署请使用自身环境的地址。原始截图与完整运行证据留在本地,仓库仅发布脱敏版本;历史实验结论不因图片脱敏而改变。 + +> 版本:V3 与控制台交互增量 +> 更新日期:2026-10-24 +> 独立修复候选:`codex/repository-review-fixes-20261023`;2026-10-24 已完成冻结候选实际完整验收与文档收口,见 11.10。老师认可的 `05312e2`、远端来源分支和原 PR 未推进。 +> 上游项目:`https://gitee.com/OpenCloudOS/ocmanager` +> 历史上游集成基线:`853dba60035d9da6c76757609f27828131d15c32` +> 历史功能提交基线:`5724fcf9cee3be0a230b0dd58f165ada852c4215` +> 历史认可版状态:V3 P9-P14 与 2026-10-14 验收作为历史基线保留;本次修复候选的最终结果独立记录在 11.10。 +> 最近完整功能验收:2026-10-24 冻结候选 152/152 PASS、核心/增强矩阵 31/31 PASS、Evidence Bundle 9/9 PASS。 +> 本次增量:界面统一、上传队列、授权恢复、错误目录、认证轮换及测试可移植性;当前候选结果来自实际完整入口,不复用历史 PASS。 +> 历史干净部署证据:Build154,基础拓扑预检 `42/42 PASS`、健康检查 `17/17 PASS`;当前能力和使用方式以本报告正文及配套部署说明为准 +> 配套文档:[方案设计](私有化软件仓库方案设计.md) · [部署说明](私有化软件仓库部署说明.md) · [测试验收](私有化软件仓库测试验收.md) +> 当前候选验收:2026-10-24 冻结候选完整发布入口退出 0,152/152 PASS、31/31 核心/增强矩阵、9/9 Evidence Bundle;文档与非文档源码一致性复核通过。`OVERALL_ACCEPTANCE=PASS`,`FINAL_READY=YES`。 + +## 目录 + +- [1. 工作总览](#1-工作总览) +- [2. 系统设计](#2-系统设计) +- [3. 数据库设计](#3-数据库设计) +- [4. 接口设计](#4-接口设计) +- [5. 前端设计](#5-前端设计) +- [6. 部署与配置](#6-部署与配置) +- [7. 使用说明与示例](#7-使用说明与示例) +- [8. 验收结果](#8-验收结果) +- [9. 最终效果](#9-最终效果) +- [10. 相关文档](#10-相关文档) +- [11. 当前版本结构化验收闭环](#11-当前版本结构化验收闭环) + +## 1. 工作总览 + +### 1.1 完成的工作 + +本项目在 OCManager 原有的 `manager/`、`tms/` 和部署体系上增加了一套完整的私有化软件仓库能力。新增功能没有建立独立于 OCManager 的第二套管理系统,而是继续使用现有项目、用户、JWT、footstone、MySQL、Vue 3、TMS/Agent 和 Compose 体系。 + +最终形成的业务闭环如下: + +```text +仓库节点注册和容量预检 +→ 按系统/产品和版本创建仓库 +→ 从上游同步或上传 RPM/tar.gz +→ RPM、架构、checksum 和 GPG 校验 +→ 损坏包进入 quarantine +→ createrepo_c 生成标准 repodata +→ immutable Revision + 相对链接原子发布 +→ 自动生成业务侧 .repo +→ OpenCloudOS 9 隔离 DNF 安装验证 +→ 清单、报告、日志、诊断和节点分发 +``` + +系统目前支持: + +- 基线仓、更新仓和私有仓的创建、编辑、查询、删除与批量定义。 +- 按系统/产品和版本筛选,以服务器分页平铺表格展示仓库。 +- 远程快照同步,支持 include、exclude 和架构过滤。 +- 上游源配置管理,内置 OpenCloudOS 官方站和国内镜像的 BaseOS/AppStream 快捷项,同时支持项目自定义地址模板和手工 URL。 +- 单个 RPM、多个文件和 `.tar.gz` 多 RPM 的 Indexed Chunk 上传、暂停与恢复。 +- RPM 完整性、NEVRA、架构、checksum、危险归档和 GPG 签名校验。 +- quarantine、按 NEVRA 删除、`PARTIAL_SUCCESS` 结果展示。 +- 标准 YUM/DNF `repodata`、不可变 Revision 和原子发布。 +- HTTP 只读发布和动态 `.repo` 生成、复制、下载。 +- 异步任务、进度、取消、重试、lease、fencing token 和同仓库互斥。 +- 可独立部署的 Repository Storage Node、磁盘/inode/工具预检和容量历史。 +- JSONL + zstd 大日志治理、摘要、脱敏、索引和授权下载。 +- 确定性规则诊断与 OCAI 辅助分析。 +- 基于现有 TMS/Agent 的不可变 Revision 节点间分发。 +- Repository 清单、校验报告和分发结果导出。 +- Restart、Redeploy、Destroy/Restore 和一键 Release Acceptance。 +- 全局任务与 Target/逐包结果、计划同步、限速并发和 MIRROR/APPEND 策略。 +- 标准 Node RPM 安装源、版本选择、TMS 安装、注册白名单和授权恢复。 +- 包签名三态策略、Repository Metadata Signing、依赖删除/安装预览和定义导入。 + +### 1.2 在原仓库中的增量 + +| 原有区域 | 新增内容 | 作用 | +| --- | --- | --- | +| `manager/backend/protocol/repository/` | `RepositoryService` Proto、生成代码和 Swagger | 对外管理接口,共 73 个 RPC | +| `manager/backend/protocol/repositorynode/` | Repository Node 内部 Proto | worker 心跳、任务领取、提交与分发闭环 | +| `manager/backend/services/footstone/presentation/` | Repository、节点、任务、内容、分发、报告、OCAI Controller | 接入 footstone 鉴权和统一响应模型 | +| `manager/backend/services/footstone/application/` | Repository 应用服务编排 | 串联项目权限、领域服务、节点和任务 | +| `manager/backend/services/footstone/domain/` | Repository、Content、Task、Node、Distribution、Diagnosis、AI 服务 | 保存业务规则和状态转换 | +| `manager/backend/services/footstone/infrastructure/repository/` | MySQL Repository、节点客户端、存储和日志实现 | 数据持久化及 mTLS 节点通信 | +| `manager/backend/services/footstone/repositoryworker/` | 同步、上传、GPG、发布、迁移、日志、分发执行器 | 在独立仓库节点处理 RPM 数据 | +| `manager/backend/cmd/repository-worker/` | 独立 worker 薄入口 | 避免把 worker 逻辑堆入 footstone 入口 | +| `manager/backend/migrations/footstone/sql/mysql/v1.0.0/` | `_5` 至 `_18` Repository 迁移 | 核心模型、产品化、传输/TMS、安全与验收闭环增量,详见第 3 章 | +| `manager/frontend/src/pages/repository/` | 仓库、上游源、节点、详情及功能组件 | 完整控制台操作页面 | +| `manager/frontend/src/service/repository.ts` | Repository 前端 API Client | 复用项目现有 `fetchApi` 和动态 JWT Header | +| Repository 任务详情与现有 OCAI SSE | Repository 会话上下文和反馈关联 | 在当前分析面板展示建议,复用既有流式协议 | +| `manager/deployments/compose/` | Repository Node Compose、Nginx 只读发布、管理面配置 | 支持独立数据节点部署 | +| `scripts/repository-*.sh` | E2E、GPG、真实安装、日志、诊断、分发、报告、恢复、总验收脚本 | 提供可重复执行的自动化入口 | +| `docs/` | 方案、架构、验收、图片和本说明 | 记录设计、使用方法与验证证据 | + +### 1.3 实施过程 + +以下为历史进度记录,旧阶段的 `FINAL READY` 仅表示当时的验收范围,不替代本版 152 项判定。 + +| 阶段 | 主要工作 | 结果 | +| --- | --- | --- | +| 8/20-8/25 | Repository CRUD、任务、同步、上传、发布、DNF、GPG、诊断、分发和恢复基线 | 旧拓扑 31/31 PASS | +| 8/26-8/28 | 重新梳理系统、数据库、接口、安全和验收设计 | V3.1/V4.0 架构冻结 | +| 8/29 | 独立仓库节点、mTLS、bind mount、原子链接和 TMS 阶梯 POC | P0 门禁通过 | +| 8/30-8/31 | Storage Node、远程任务数据面、两阶段提交、容量预检和大日志治理 | 核心数据面迁至独立节点 | +| 9/1 | 前端节点管理、容量、日志、OCAI 上下文与真实页面联调 | 前后端闭环通过 | +| 9/2-9/3 | OCAI 实际模型、官方 RPM 分级同步、TMS/Agent 分发和恢复 | 新拓扑主要链路通过 | +| 9/4 | Build100 全量回归、协议重复生成和 Release Acceptance | 31/31 PASS,FINAL READY | +| 9/5-9/11 | 上游源产品化、架构矩阵、删除闭环、任务恢复和官方 OC9 镜像复验 | Build130 31/31 PASS | +| 9/12 | 同步过滤容错、上传失败终态、国内镜像源、上游集成与完整复验 | Build143 31/31 PASS,final_ready=true | +| 9 月下旬至 10/14 | 节点运维、任务聚合、断点续传、计划同步、TMS 安装、GPG Metadata、导入与 Node RPM 白名单闭环 | 最终候选 152 项逐项验收通过;具体证据见第 11 章 | + +## 2. 系统设计 + +### 2.1 总体架构 + +![OCManager 私有化软件仓库总体架构](images/repository/repo-architecture.png) + +图 1:控制台、管理面、仓库节点、TMS/Agent 与 OCAI 的职责和数据流。[查看矢量图](images/repository/repo-architecture.svg)。 + +![前端入口到后端能力的功能映射](images/repository/repo-feature-map.png) + +图 2:六条主要操作路径对应的页面入口、真实 RPC、后端模块和结果,可与第 4 章接口设计、第 5 章页面操作对照。[查看矢量图](images/repository/repo-feature-map.svg)。 + +系统分为管理面、仓库数据面、发布面、节点分发面和诊断面。 + +| 层次 | 组件 | 职责 | +| --- | --- | --- | +| 控制台 | Vue 3 + TDesign Vue Next | 仓库、节点、任务、包、GPG、分发、报告和 OCAI 操作 | +| 网关 | OCManager Nginx | 转发管理 API 和 OCAI;Repository 公共数据不经过上传入口 | +| 管理面 | footstone Repository API | 鉴权、项目隔离、模型、任务、元数据、报告和状态协调 | +| 元数据 | MySQL/footstone | Repository、Task、Revision、Package、Node、日志索引等结构化数据 | +| 数据面 | `repository-worker` | 同步、分片接收、RPM/GPG 校验、repodata、Revision、日志和迁移 | +| 发布面 | Repository Node Nginx | 从只读 bind mount 提供 `GET/HEAD` 仓库访问 | +| 分发面 | TMS + Existing Agent | 把指定 immutable Revision 下发到目标节点并返回执行结果 | +| 诊断面 | 规则引擎 + `ocai-service` | 先产生稳定错误分类,再基于脱敏上下文给出原因和建议 | + +### 2.2 部署拓扑 + +部署按管理面、仓库节点和 TMS Agent 三种逻辑角色组织,实际可同机或分机;三种角色不等于必须三台机器。下图展示分离部署示例,地址由部署环境提供: + +```text +管理节点 () + ├─ Vue / Nginx + ├─ footstone / MySQL + ├─ TMS + └─ ocai-service + +源仓库节点 () + ├─ repository-worker + ├─ repository-publisher :18090 + └─ /opt/ocmanager/repository-data + +目标节点 () + └─ Existing TMS Agent +``` + +管理服务只保存仓库模型、任务状态、包清单和日志索引。RPM、repodata、quarantine 和原始大日志由 Repository Node 持久化;同机部署时仍使用独立节点服务和数据目录,不写进 Footstone 的应用目录。没有 Agent 时可使用普通上传/同步/发布,远程安装与分发需要先接入 TMS Agent。 + +### 2.3 后端分层 + +Repository 能力遵循 OCManager 现有分层: + +```text +presentation + 接收 RepositoryService 请求、校验权限、转换响应 + ↓ +application + 编排 Repository、Task、Node、Content、Distribution、OCAI 服务 + ↓ +domain + 保存仓库状态、任务规则、锁语义、诊断和发布约束 + ↓ +infrastructure + MySQL Repository、mTLS Node Client、TMS Client、日志与文件实现 +``` + +worker 使用相同 Go module,但通过 `manager/backend/cmd/repository-worker/` 独立启动。实际执行逻辑放在 `services/footstone/repositoryworker/`,footstone 主入口只负责装配,不包含大段 worker 分支。 + +### 2.4 仓库节点与容量预检 + +仓库创建时必须选择主仓库节点。节点每 10 秒发送心跳,30 秒没有新心跳即视为离线。同步、上传、删除包、重建元数据、迁移和发布前必须有未过期的 Preflight。 + +仓库节点与分发目标 Agent 使用不同身份。首个 Repository Node 可以手工安装和注册,后续可由仓库节点页通过标准 Node RPM 源、TMS 和 Enrollment 自动安装注册。分发目标通过 TMS asset-manager 注册、heartbeat 判活。`ListRepositoryDistributionNodes` 查询的是 TMS Agent,不是 `repository_storage_nodes`。同一机器可以兼任角色,但创建仓库节点不会凭空生成 Agent。 + +预检内容包括: + +- 文件系统类型及同文件系统 rename、相对 symlink 能力。 +- 总容量、可用空间、使用率和可用 inode。 +- `dnf/reposync`、`createrepo_c`、`rpm/rpmkeys`、`gpg` 等工具版本。 +- 上游 metadata 推导出的包数量和导入大小;无法推导时使用显式上限。 +- 新 Revision、staging、metadata 和安全余量形成的峰值空间。 + +容量公式为: + +```text +required_free = + active_revision_size + + max_import_expanded_size + + projected_new_revision_size + + metadata_reserve + + safety_margin +``` + +默认安全余量为 20%,文件系统使用率达到 90% 或 inode 不足时拒绝新的 mutation,但已发布 Repository 仍可正常读取。 + +### 2.5 数据目录 + +Repository Node 使用宿主机 bind mount: + +```text +/opt/ocmanager/repository-data + -> /var/lib/ocmanager/repository-data +``` + +目录结构如下: + +```text +repository-data/ +├── staging/ +│ └── uploads//content +├── quarantine/ +├── revisions-private/ +├── task-logs// +│ ├── execution.jsonl.zst +│ ├── execution.meta.json +│ └── result.jsonl.zst +├── keys///public.key +├── trash/ +└── public// + ├── keys//public.key + ├── revisions// + │ ├── *.rpm + │ ├── manifest.json + │ └── repodata/repomd.xml + └── current -> revisions/ +``` + +Repository worker 使用读写挂载,Repository Publisher 只挂载 `public/` 且为只读;管理面容器不挂载 RPM 数据目录。控制节点宿主机可以兼任 Repository Node,但 RPM 仍归属于 Repository Node 的独立 bind 目录。私钥不进入该目录。 + +### 2.6 同步流程 + +Remote Sync 使用快照语义: + +```text +SyncRepository +→ 节点/容量/工具预检 +→ Repository Mutation Lock +→ 下载上游 repodata +→ include/exclude 和 architecture 过滤 +→ RPM 下载、checksum、格式和 GPG 校验 +→ staging 构建完整候选 Revision +→ createrepo_c +→ manifest/repomd SHA-256 +→ PrepareTaskCommit +→ current 相对链接原子切换 +→ ConfirmTaskCommit +``` + +任一关键 RPM 下载、checksum、签名或 metadata 失败时,整个同步任务失败,候选 Revision 不发布,因此不会出现“同步了一半但已经对外可用”的仓库。 + +### 2.7 上传流程 + +上传使用“管理 API 建立会话 → mTLS 转发到绑定节点 → 分片写入 staging → 完成校验 → 异步入库”。 + +- 当前前端使用 4 MiB Indexed Chunk,每个文件独立 Session,最多两个文件并发传输。 +- `upload_id + chunk_index` 唯一,相同摘要重复提交幂等;不同摘要冲突拒绝。允许乱序和缺失分片补传,完成时校验全部分片及完整文件 SHA-256。 +- 旧 Append 接口仅作兼容,不再作为当前 UI 的顺序上传设计。页面刷新可恢复 Session 信息,必要时由用户重新选择同一 File。 +- 单 RPM 全部通过后形成新 Revision。 +- `.tar.gz` 限制归档条目数和解压后大小,拒绝绝对路径、`..`、链接和危险文件。 +- tar 中部分 RPM 合法时允许 `PARTIAL_SUCCESS`:合法包进入新 Revision,非法包进入 quarantine。 +- 全部包无效时任务失败,active Revision 不变。 + +### 2.8 Repodata 与原子发布 + +![repodata 与原子发布流程](images/repository/repo-publish-flow.png) + +图 3:内容校验、repodata、可选元数据签名、Prepare/Activate/Confirm 和原子发布的执行边界。[查看矢量图](images/repository/repo-publish-flow.svg)。 + +每次修改都创建不可变 Revision,原 Revision 不原地修改。worker 在 staging 完成 RPM、manifest 和 `createrepo_c` 输出校验后,将目录转入 `public//revisions/`,再通过同文件系统 rename 原子替换相对 `current` 链接。 + +发布地址有两种: + +```text +稳定地址:/repositories//current/ +不可变地址:/repositories//revisions// +``` + +客户端日常使用稳定地址;分发、验收和取证始终绑定不可变地址、Revision ID、manifest SHA-256 和 repomd SHA-256。 + +### 2.9 任务、锁和恢复 + +![Repository 异步任务状态](images/repository/repo-task-state.png) + +图 4:Task、Target 和逐项结果的状态关系,以及取消、重试和并发保护。[查看矢量图](images/repository/repo-task-state.svg)。 + +任务类型覆盖 `SYNC`、`UPLOAD`、`DELETE_PACKAGE`、`DELETE_REPOSITORY`、`REBUILD_METADATA`、`PUBLISH`、`VALIDATE`、`DISTRIBUTE`、`MIGRATE_REPOSITORY`、`NODE_INSTALL`、`PACKAGE_INSTALL` 和 `INVENTORY_IMPORT`。Task 是统一业务状态来源,Task Target 保存节点执行结果,Task Result Item 保存逐包结果;Import Job 仅保存领域细节。 + +任务状态包括: + +```text +PENDING → RUNNING → SUCCESS + → PARTIAL_SUCCESS + → FAILED + → CANCELED +``` + +同一 Repository 的 mutation 必须获得 `repository_mutation_locks`。锁使用 lease 和单调递增的 fencing token,过期 worker 即使继续运行,也不能用旧 token 发布结果。不同 Repository 可以并发执行。 + +跨节点发布采用两阶段提交: + +```text +worker PrepareTaskCommit +→ footstone 导入 Revision/Package/Quarantine 元数据 +→ worker ActivateRevision +→ worker ConfirmTaskCommit +→ software_repositories.active_revision_id 更新 +``` + +若链接已经切换但 Confirm 失败,恢复任务会根据 `current` 链接、PREPARED 记录、Revision 和 manifest 重新对账,而不是盲目重复发布。 + +### 2.10 日志与诊断 + +原始任务日志使用 JSONL 流式写入,并用 zstd level 3 压缩。数据库只保存摘要、路径、SHA-256、行数、压缩前后大小、错误指纹和过期时间。 + +默认策略: + +- 成功日志保留 14 天,失败日志保留 30 天。 +- 数据库摘要不超过 64 KiB。 +- OCAI 上下文不超过 32 KiB。 +- Authorization、URL 密码、token、secret、API Key 和上传数据先脱敏。 + +规则诊断输出固定结构: + +```text +Failure Category +Error Code +Failed Step +Likely Cause +Evidence +Recommended Action +``` + +OCAI 只读取脱敏后的任务事实、规则诊断和有限日志窗口,输出原因、证据和建议。Retry、Re-sync、Rebuild 等操作仍由用户通过受 JWT 和 Mutation Lock 保护的 Repository API 发起。 + +### 2.11 GPG 与 HTTP 安全 + +当前实现支持: + +- `DISABLED` +- `AUDIT` +- `ENFORCE`,兼容旧 `VERIFY_PACKAGE` 的严格校验语义 + +`repository_gpg_keys` 只保存 key ID、fingerprint 和公钥路径。公钥原文保存在 Repository Node,允许客户端下载的副本发布到 public 路径。Metadata Signing 私钥仅在 Node 本地的受限密钥目录,Manager 只保存 key_ref/公开指纹;证书表只保存公开证书和恢复所需元数据。Git、MySQL、公共 Repository 和 Evidence 均不保存私钥。 + +元数据签名发布 `repomd.xml.asc` 和公开 Key;Generator 仅在真实签名材料完整时输出 `repo_gpgcheck=1`。Key 轮换只影响新 Revision,历史 immutable Revision 保持原签名。 + +Repository Publisher 只允许 `GET/HEAD`,`POST/PUT/PATCH/DELETE` 被拒绝,`autoindex off`,并添加 `X-Content-Type-Options: nosniff`。上传必须走 JWT 管理接口,不能从 `/repositories/` 写入。 + +## 3. 数据库设计 + +![Repository 领域模型](images/repository/repo-er-model.png) + +图 5:仓库内容、节点占用、统一任务、上传分片、Node 安装授权和 GPG 签名之间的关系,具体表和约束见下文。[查看矢量图](images/repository/repo-er-model.svg)。 + +本报告架构配图同时提供 PNG、SVG 与同名可编辑 `.drawio` 导出,统一由 `scripts/render-repository-diagrams.mjs` 生成;PNG 用于正文和邮件/PDF,SVG 用于放大查看。维护入口及导出命令见[配图说明](images/repository/README.md)。 + +### 3.1 数据与文件的边界 + +MySQL 保存仓库定义、任务、不可变版本索引、包清单、节点身份和运行记录;Repository Node 文件系统保存 RPM、repodata、隔离文件及原始压缩日志。数据库不保存 RPM 文件、GPG 私钥、节点私钥或 CA 私钥。 + +`software_repositories.active_revision_id` 指向当前发布版本。`repository_revisions` 固定 manifest 和 repomd 摘要,`repository_packages` 保存该版本的 RPM 清单。Worker 先构建候选版本,再通过 Prepare/Activate/Confirm 闭环协调数据库记录与节点 `current` 相对链接。 + +### 3.2 仓库、内容和运行记录 + +| 表 | 保存内容 | 关键关系或约束 | +| --- | --- | --- | +| `software_repositories` | 项目、系统/产品、版本、类型、架构、上游、过滤规则、GPG、节点、当前 Revision、同步模式和限速 | 同一项目/产品/版本/名称/架构唯一;`publish_key` 是稳定发布路径键 | +| `repository_upstream_profiles` | 项目自定义来源的 URL 模板、适用版本/架构和默认过滤规则 | 只管理配置;选择后解析到 Repository,不改变已建仓库 | +| `repository_revisions` | parent、manifest/repomd 摘要、包数、大小、节点和签名结果 | immutable Revision;已发布版本不随新签名 Key 修改 | +| `repository_packages` | NEVRA、路径、大小、SHA-256、来源、上传会话及签名结果 | `(revision_id, nevra)` 唯一 | +| `repository_uploads` | 文件名、总大小、节点、接收状态、fingerprint、chunk 参数和过期时间 | 每个文件一个 Session;上传位置不可漂移 | +| `repository_upload_chunks` | `upload_id, chunk_index, size_bytes, sha256, stored_path` | `(upload_id, chunk_index)` 主键,是已接收分片的事实来源 | +| `repository_quarantine_items` | 被拒绝文件的路径、大小、摘要、原因及可识别 NEVRA | 不进入可用 Revision;不参与已发布包清单 | +| `repository_gpg_keys` | `id, repository_id, key_id, fingerprint, public_key_path, created_at` | 只保存公钥元数据,无 key body 或私钥 | +| `repository_storage_nodes` | 控制/发布地址、证书指纹、状态、容量、inode、工具、OS/架构、版本、心跳和错误 | 项目隔离;节点文件系统容量是磁盘统计依据 | +| `repository_node_repository_usages` | 节点上每个仓库的 logical/physical bytes、发布状态和观测时间 | `(storage_node_id, repository_id)` 主键 | +| `repository_preflight_records` | 容量、inode、工具、文件系统预检和有效期 | Task 绑定预检,执行前再次轻量复核 | +| `repository_task_log_indexes` | 原始日志相对路径、SHA-256、压缩前后大小、行数、摘要、错误指纹、保留期 | 原始日志留在节点,不作为数据库大字段存储 | +| `repository_ai_analyses` | OCAI session、上下文 SHA-256、状态及操作反馈 | 关联失败 Task 与后续 action Task,无模型凭据 | +| `repository_distributions` / `repository_distribution_targets` | 固定 Revision/manifest 的分发及逐 Agent 结果 | 保留既有分发领域模型;目标状态、退出码和输出可分别查询 | + +`package_count`、`logical_bytes`、最近同步和版本数量是后端查询得到的汇总,不是 Vue 扫描目录或对当前分页临时累加。`logical_bytes` 表示 active Revision 的 RPM 内容大小;`physical_bytes` 表示节点上可归属到该仓库的 Revision、staging、quarantine、metadata、trash 实际分配空间。Node 总量和可用量来自文件系统探测,不能通过包大小相加代替。 + +### 3.3 Task、Target、逐包结果和调度 + +| 表 | 职责 | 关键字段或约束 | +| --- | --- | --- | +| `repository_tasks` | 整体业务操作的状态、进度、错误、开始/结束时间及发布提交状态 | `scope_type/scope_id/executor_kind`;`trigger_source`;nullable unique `idempotency_key`;lease/fencing;`storage_node_id/preflight_id`;prepared/activated/confirmed 字段 | +| `repository_mutation_locks` | 同仓库写操作互斥 | `repository_id` 主键;持有 Task 和 fencing token | +| `repository_task_targets` | 一个业务 Task 下具体 Node/Agent 的执行结果 | `target_type/target_id`、node/agent、进度、时间、错误、`result_json`;`(task_id,target_type,target_id)` 唯一 | +| `repository_task_result_items` | 批量删除中每个包的结果 | `task_id,item_key,package_nevra,status,error_code,message,result_json,started_at,finished_at`;`(task_id,item_key)` 唯一 | +| `repository_sync_schedules` | 每个仓库的每日/每周计划 | `cadence,timezone,day_of_week,hour,minute,status,next_run_at,last_run_at`、连续失败及阈值;`repository_id` 唯一 | +| `repository_schedule_runs` | 一次到期调度的幂等记录 | unique `schedule_run_key`,关联 Task,保存跳过/失败原因 | + +多个 Scheduler 通过唯一 Run Key 与任务幂等键避免重复建任务。仓库已有同步时记录 `SKIPPED_ALREADY_RUNNING`,不制造额外失败任务。计划支持 `ENABLED/PAUSED/AUTO_PAUSED`,连续失败达到阈值后自动暂停。 + +Task Target 用于 Node 安装与 Package Install,逐目标保存执行结果;批量删包使用 Result Item,一次操作只创建一个 `DELETE_PACKAGE` Task 并统一重建一次 repodata。两者分别表达“在哪个节点执行”和“哪个包处理成功”,不建立第二套业务任务系统。 + +### 3.4 Node 安装、授权和证书 + +| 表 | 职责 | 关键字段或约束 | +| --- | --- | --- | +| `repository_node_install_sources` | 项目/架构的正式 Node RPM 软件源 | `project_id,repository_id,architecture,status`;项目/架构唯一;引用 Repository 时限制删除 | +| `repository_node_artifacts` | Node RPM 的版本、架构、完整性及标准仓库发布位置 | `project_id,source_kind,source_repository_id,revision_id,package_id,epoch,version,release,nevra,sha256,build_commit,immutable_url,status` | +| `repository_node_registration_allowlists` | 默认拒绝的新注册授权 | `(project_id,tms_agent_id)` 唯一;可选 hostname/IP/architecture 约束、启用状态和有效期 | +| `repository_node_enrollments` | 预分配节点身份和安装/注册过程 | `node_id,generation,install_task_id,tms_task_id,expected_architecture,allowlist_entry_id,artifact_id` 及授权恢复状态 | +| `repository_node_bootstrap_tokens` | 一次性、短时效的首次授权凭据 | 只存 `token_hash`;关联 enrollment/task/agent;记录消费、失效和过期时间 | +| `repository_node_certificates` | 唯一公开证书权威来源 | serial/fingerprint、有效期、状态、宽限期;v18 增加 `authorization_id,certificate_pem,certificate_chain_pem,issuer_fingerprint` | +| `repository_tms_capabilities` | TMS Agent 能力快照 | agent、canonical architecture、OS、在线状态和观测时间 | + +Node RPM 源仍是普通 Private Repository,额外使用 `system_managed=true,purpose=NODE_RPM` 保护系统用途。正式 Artifact 必须来自项目和架构匹配的启用 Install Source,绑定合法的 Node RPM、immutable Revision 和 SHA-256。历史本地 Artifact 标记为 `LEGACY_LOCAL`,不作为新安装的默认来源。默认最新版本按 RPM EVR 比较。 + +Enrollment 的 `authorization_id,csr_sha256,authorized_at,issuance_status,issuance_lease_owner,issuance_lease_expires_at` 保存恢复状态,不另存第二份证书。事务 A 锁定相关授权记录、校验并原子消费 Token 后提交,证书在事务外签发;事务 B 保存证书并完成注册。精确相同的授权与 CSR 可恢复未完成签发或返回原公开证书,普通 Token Replay 必须拒绝。 + +### 3.5 Preview、Package Install、GPG 与 Import + +| 表 | 职责 | 关键字段或约束 | +| --- | --- | --- | +| `repository_operation_previews` | 删除、安装和导入的短时预览 | operation/resource、`resource_version,preview_digest,payload,expires_at,created_by,status,confirmed_at` | +| `repository_package_installs` | 安装领域数据及 TMS 关联 | 固定 repository/revision/preview,关联唯一 `repository_task_id` | +| `repository_metadata_signing_keys` | 节点可用元数据签名 Key Inventory | `(storage_node_id,key_ref)` 唯一;fingerprint、公钥名/URL、状态和观测时间,无私钥 | +| `repository_inventory_import_jobs` | 导入映射、预览和逐对象结果 | preview、schema、冲突策略、mapping/summary;v18 以 `repository_task_id` 关联全局权威 Task | + +Repository 通过 `metadata_signing_key_ref` 选择节点已有 Key。Revision 记录真实签名结果、指纹、`repomd_signature_path` 和 `metadata_public_key_url`。更换 Key 只影响以后新建 Revision,不重写历史版本。Package 保存 `signature_status,signer_key_id,signature_message`;`VERIFY_PACKAGE` 是旧策略兼容入口,现有策略包含 `DISABLED/AUDIT/ENFORCE`。 + +Inventory 导入导出表示配置迁移,不是文件/数据库完整备份。节点与 Key 需要目标环境重新映射;不恢复 RPM、Revision 内容、私钥、Secret 或任务历史。Confirm 创建 `INVENTORY_IMPORT` Task,Job 只保存 Import 领域数据。 + +### 3.6 Migration 与升级边界 + +| Migration | 本次数据库增量 | +| --- | --- | +| `v1.0.0_15_repository_productization.sql` | Node OS/架构/版本、Repository 同步模式/限速/并发、Task trigger/幂等键、Node Repository Usage、Schedule/Run | +| `v1.0.0_16_repository_transfer_and_tms.sql` | Task scope/target、Enrollment/Token/Certificate、Artifact、TMS Capability、Upload Session/Chunk、Operation Preview、Package Install | +| `v1.0.0_17_repository_gpg_import.sql` | Package 签名结果、metadata Key Inventory、Revision 签名字段、Inventory Import Job | +| `v1.0.0_18_repository_acceptance_closure.sql` | 系统用途保护、Install Source、Allowlist、Enrollment 授权恢复、原证书表公开证书恢复字段、Repository-backed Artifact、Task Result Item、Import Task 关联及约束 | + +增量采用安全默认值与 nullable 兼容引用,旧节点、旧 Artifact 和旧导入记录可升级。v18 已作为整体 Schema 冻结,后续新增需求应创建新 Migration。正式表结构以 `manager/backend/migrations/footstone/sql/mysql/v1.0.0/` 和 `manager/backend/services/footstone/domain/repository/po/` 为准。 + +## 4. 接口设计 + +### 4.1 协议与权限 + +管理接口由 `manager/backend/protocol/repository/repository.proto` 定义,沿用 Footstone 的 tRPC-Go、JWT 和项目权限: + +```text +POST /trpc.ocmanager.repository.RepositoryService/ +Content-Type: application/json +<登录返回的 jwt_key>: +``` + +请求直接使用 Proto 字段,`bytes` 通过 JSON 调用时使用 Base64,时间用 UTC Timestamp/RFC3339 传输,分页使用 `page_request`。响应沿用 `success/error_code/return_code/error_message/data` 封装;下载 API 返回文件流。用户身份和项目范围在后端校验,Vue 不能通过隐藏按钮替代授权。 + +内部 Node RPC 使用独立 mTLS 服务,首次 Bootstrap 使用独立 TLS 入口和一次性授权;公共 Repository 下载只开放 `GET/HEAD`。三类入口不共用 JWT 或身份模型。 + +### 4.2 Repository 与上游来源 + +| RPC | 主要请求 | 返回和行为 | +| --- | --- | --- | +| `CreateRepository` / `UpdateRepository` | `spec`;更新另带 `repository_id` | 返回 `repository`;保存配置并进行项目、节点、架构和策略校验 | +| `DescribeRepository` | `repository_id` | 仓库配置、active Revision、包数、逻辑大小、最近同步和版本数 | +| `ListRepositories` | `project_id,system_product,product_version,architecture,type,keyword,page_request` | `repositories,page_response`;服务器分页平铺结果 | +| `DeleteRepository` | `repository_id,confirm_delete` | empty;检查 active 包、未过期的未完上传、GPG 公钥及运行任务;隔离项随仓库记录清理,历史 Revision 本身不是“仍有包”的依据 | +| `BatchApplyRepositories` | `items[{repository_id,spec}]` | `results[{input_index,operation,repository}]`;空 ID 创建,非空 ID 更新,复用相同 Service | +| `CreateRepositoryUpstreamProfile` / `UpdateRepositoryUpstreamProfile` | `spec`;更新另带 `upstream_profile_id` | 创建或更新项目来源模板 | +| `DescribeRepositoryUpstreamProfile` / `DeleteRepositoryUpstreamProfile` | `upstream_profile_id` | 单项查询或删除配置,不改已创建仓库 | +| `ListRepositoryUpstreamProfiles` | `project_id,system_product,keyword,enabled,page_request` | 自定义来源列表和分页 | + +`RepositorySpec` 包含 `project_id,system_product,product_version,name,description,architecture,type,upstream_url,allow_upload,include_packages,exclude_packages,gpg_policy,enabled,primary_storage_node_id,sync_mode,bandwidth_limit_bytes,max_parallel_downloads,metadata_signing_key_ref`。`system_managed/purpose` 由专用系统源流程控制,普通更新接口不能修改系统源关键字段。 + +`RepositoryUpstreamProfileSpec` 包含项目、名称、产品、channel、`url_template,supported_versions,supported_architectures,default_include_packages,default_exclude_packages,enabled`。HTTP(S) 模板只允许版本和架构占位符,拒绝 URL 密码及 fragment。内置来源与用户自定义来源共同服务于选择表单,最终 Worker 仍读取 Repository 上已保存的地址与过滤条件。 + +### 4.3 Node 运维、容量和安装 + +| RPC | 主要请求 | 返回和行为 | +| --- | --- | --- | +| `CreateRepositoryStorageNode` / `UpdateRepositoryStorageNode` | node `spec`;更新带 `storage_node_id` | 名称、控制/发布地址、TLS server name、指纹、标签和启用状态 | +| `DescribeRepositoryStorageNode` | `storage_node_id` | `storage_node,repositories[],metadata_signing_keys[]` | +| `ListRepositoryStorageNodes` | `project_id,status,keyword,page_request` | nodes 与完整筛选集合的 `total_nodes,online_nodes,total_capacity_bytes,used_capacity_bytes,critical_nodes`,非当前页累加 | +| `DeleteRepositoryStorageNode` | `storage_node_id,confirm_delete` | 有关联仓库时拒绝删除 | +| `ProbeRepositoryStorageNode` | `storage_node_id` | 最新 Node 和 Preflight;探测容量/inode/工具/文件系统 | +| `ListRepositoryPreflightRecords` | node、可选 repository、分页 | 历史预检记录 | +| `ConfigureRepositoryNodeInstallSource` | `spec{project_id,repository_id,architecture,status}` | 正式 Install Source,保护普通 Private Repository 为 Node RPM 系统源 | +| `ListRepositoryNodeInstallSources` | `project_id` | 项目各架构安装源 | +| `RegisterRepositoryNodeArtifact` | Artifact `spec` | 校验并登记版本、架构、SHA-256、构建 Commit,以及 repository/revision/package 绑定 | +| `ListRepositoryNodeArtifacts` | `project_id,status` | Artifact 列表;含 `source_kind,source_repository_id,revision_id,package_id,epoch,nevra,immutable_url` | +| `DownloadRepositoryNodeArtifact` | `artifact_id` | 授权下载 Node RPM;正常安装仍使用 Repository immutable 源 | +| `UpsertRepositoryNodeRegistrationAllowlist` | `spec{project_id,tms_agent_id,expected_hostname,expected_ip,expected_architecture,status,expires_at}` | 创建/更新授权,包括禁用;Agent 是强身份,IP/hostname 是附加约束 | +| `ListRepositoryNodeRegistrationAllowlists` | `project_id` | 当前项目白名单 | +| `CreateRepositoryNodeEnrollment` | `spec{project_id,name,description,control_base_url,publish_base_url,tls_server_name,labels,expected_architecture,tms_agent_id,artifact_id}` | `storage_node,enrollment,task,target,bootstrap_token,bootstrap_expires_at`;预分配稳定节点身份,创建 Node 安装任务 | + +Node 详情中的 `repositories[]` 返回仓库 ID/名称、产品版本、类型/架构、上游、包数、`logical_bytes/physical_bytes`、Revision、最近同步/下次同步、运行任务及错误。Node Drawer 的“同步”直接调用 `SyncRepository`;最近任务使用 `ListRepositoryTasks(storage_node_id=当前节点)`,没有第二套 Node Task API。 + +Node 安装默认选择当前项目/架构启用 Install Source 的合法已发布 Artifact,按 RPM EVR 决定最新版本,用户可指定历史 Artifact。后端再次核对 Agent 与 RPM canonical architecture。TMS 传送小脚本和临时源配置,由目标 DNF 从 immutable Revision 安装,沿用实际 GPG 策略并清理临时源。 + +### 4.4 Sync、Schedule 与 Repo Config + +| RPC | 主要请求 | 返回和行为 | +| --- | --- | --- | +| `SyncRepository` | `repository_id` | `task`;复用预检、Mutation Lock、Worker 和 atomic publish | +| `UpsertRepositorySyncSchedule` | `repository_id,spec` | 每日/每周计划、时区、时间、失败阈值和状态 | +| `DescribeRepositorySyncSchedule` | `repository_id` | last/next run、连续失败和错误 | +| `PauseRepositorySyncSchedule` / `ResumeRepositorySyncSchedule` | `repository_id` | 暂停/恢复同一计划 | +| `GenerateRepositoryConfig` | `repository_id,immutable_revision` | `config{repo_id,file_name,content,baseurl,revision_url,enabled,gpgcheck,repo_gpgcheck,gpgkey_urls,active_revision_id}` | +| `RegisterRepositoryGPGKey` | `repository_id,public_key` | 公钥元数据与发布 URL;`public_key` 是 bytes | +| `ListRepositoryGPGKeys` | `repository_id` | keys | +| `DeleteRepositoryGPGKey` | `repository_id,key_id,confirm_delete` | 删除公钥引用和副本 | + +同步保持完整快照发布语义;MIRROR/APPEND、限速和下载并发被复制为 Task 实际配置。只有已发布 Revision 确实具备签名与可访问公钥时 Repo Config 才输出 `repo_gpgcheck=1`。固定版本配置使用 immutable URL,普通业务配置可使用稳定 `current` URL。 + +### 4.5 全局 Task、日志、诊断与 OCAI + +| RPC | 主要请求 | 返回和行为 | +| --- | --- | --- | +| `ListRepositoryTasks` | `project_id,repository_id,storage_node_id,type,trigger,status,created_at_from,created_at_to,page_request` | `tasks,statistics,page_response`;四态统计保留其余筛选条件,但不受 status filter 影响 | +| `DescribeRepositoryTask` | `task_id` | Task、Targets、Result Items、diagnosis、提交阶段、英文错误码和统一中文原因/建议 | +| `CancelRepositoryTask` / `RetryRepositoryTask` | `task_id` | 取消信号或带 retry 关系的新 Task | +| `ExportRepositoryTaskDiagnosis` | `task_id` | 结构化诊断 JSON 与摘要 | +| `DescribeRepositoryTaskLog` | `task_id` | 摘要、SHA-256、压缩前后大小、行数、错误指纹和过期时间 | +| `DownloadRepositoryTaskLog` | `task_id` | 后端统一脱敏的 `repository-task--logs.tar.gz` | +| `BuildRepositoryOCAIContext` | `task_id` | 可预览、脱敏、有界的分析上下文 | +| `CreateRepositoryOCAIAnalysis` | `task_id` | 独立 analysis/session 与固定分析指令 | +| `DescribeRepositoryOCAIAnalysis` | `analysis_id` | 会话和关联结果 | +| `AppendRepositoryOCAIFeedback` | `session_id,action_task_id,action_result` | 记录人工操作反馈及任务结果 | + +日志 Bundle 包含 `context.json,result-summary.json,task-log.jsonl.zst,SHA256SUMS`。下载前验证项目权限与保留期,再由后端统一清理 Token、Authorization、密码、URL 凭据和私钥等敏感信息。前端不把 Drawer 文本拼成“完整日志”。 + +OCAI 流式对话仍由既有 `/api/v2/ocai/chat` SSE 提供。Repository 只构造上下文和记录会话,重试/同步/重建仍走授权管理 API。错误中文内容统一来自后端 `repositoryErrorCatalog`,未知 Code 保留原文和技术 message,并返回通用中文说明/建议。 + +### 4.6 Indexed Chunk 与多文件上传 + +| RPC | 主要请求 | 返回和行为 | +| --- | --- | --- | +| `BeginRepositoryUpload` | `repository_id,file_name,total_size,expected_sha256?,client_fingerprint,chunk_size` | Session;完整文件 SHA 可选,不要求浏览器上传前扫描几十 GiB | +| `PutRepositoryUploadChunk` | `upload_id,chunk_index,chunk_sha256,data` | `upload,chunk`;允许乱序,重复同摘要幂等,不同摘要拒绝 | +| `DescribeRepositoryUploadChunks` | `upload_id` | `upload,chunks,missing_chunk_indexes` | +| `PauseRepositoryUpload` / `ResumeRepositoryUpload` | `upload_id` | 暂停/恢复 Session,不丢已确认分片 | +| `AppendRepositoryUploadChunk` | `upload_id,offset,data` | 顺序旧客户端兼容;新前端使用 Indexed Chunk | +| `CompleteRepositoryUpload` | `upload_id` | 完整性校验后返回 `upload,task`,进入原 RPM/archive 处理链 | +| `AbortRepositoryUpload` / `DescribeRepositoryUpload` | `upload_id` | 中止/清理或查看 Session | + +RPC `bytes data` 在 JSON 中为 Base64,单片受大小限制。每个文件独立 Session,前端默认最多并发 2 个文件,总进度按已上传字节/总字节计算。刷新后可恢复服务端 Session;若浏览器失去 File 引用,用户重新选择同一文件后仅补 missing chunks。 + +### 4.7 软件包、依赖删除与远程安装 + +| RPC | 主要请求 | 返回和行为 | +| --- | --- | --- | +| `ListRepositoryPackages` | `repository_id,revision_id?,name,architecture,page_request` | NEVRA、版本/架构、来源、大小、SHA-256 和签名结果 | +| `ListRepositoryQuarantine` | `repository_id,task_id?,reason_code?,page_request` | 被拒绝包、错误及检测结果 | +| `DeleteRepositoryPackage` | `repository_id,nevra` | 兼容单包删除 Task | +| `PreviewRepositoryPackageDeletion` | `repository_id,nevras[]`(最多 100) | 固定 `revision_id,repository_set,preview_id,preview_digest,expires_at,created_by`,直接/传递依赖影响、命令和 raw output | +| `ConfirmRepositoryPackageDeletion` | `preview_id,preview_digest` | 一个 DELETE_PACKAGE Task,逐包 result_items,一次 metadata rebuild | +| `PreviewRepositoryPackageInstall` | `repository_id,revision_id?,package_names[],target_agent_ids[],timeout_seconds` | 每个目标独立 Preview;选中/依赖/升级/替换包、下载字节、磁盘变化、退出码和 raw output | +| `ConfirmRepositoryPackageInstall` | `preview_id,preview_digest` | `install,task`;一个业务 Task、多个 Target | +| `DescribeRepositoryPackageInstall` | `install_id` | install 领域信息与 Task/Target 执行结果 | + +Preview/Confirm 在后端重新验证操作者、Digest、有效期及资源版本。删除影响分析只启用绑定 Revision 集合;安装 Preview/Confirm/Execute 都固定同一 immutable URL,DNF 输出固定 `LC_ALL=C,LANG=C`,关键交易字段由后端解析。资源变更或预览过期必须重新 Preview,不静默级联删除。 + +### 4.8 分发、配置导入导出与迁移 + +| RPC | 主要请求 | 返回和行为 | +| --- | --- | --- | +| `ListRepositoryDistributionNodes` | 空请求 | TMS/Agent 目标列表,不读取 Storage Node 表 | +| `CreateRepositoryDistribution` | `repository_id,revision_id,target_ips[],timeout_seconds` | `distribution,task`,固定 manifest/repomd 摘要 | +| `DescribeRepositoryDistribution` / `ListRepositoryDistributions` | distribution ID 或 repository + 分页 | 分发及逐目标 Agent、退出码、错误、输出 | +| `ExportRepositoryInventory` | `project_id` | `file_name,content_type,sha256,content`,v2 配置清单 | +| `PreviewRepositoryInventoryImport` | `project_id,content,conflict_strategy,default_storage_node_id,storage_node_mappings[],signing_key_mappings[]` | Preview ID/Digest、schema/resource version、有效期、逐项 CREATE/SKIP/CONFLICT/INVALID 和 warnings | +| `ConfirmRepositoryInventoryImport` | `preview_id,preview_digest` | `job,repositories`;Job 的 `repository_task_id` 可在全局任务中查询 | +| `ExportRepositoryValidationReport` | `repository_id` | 量化验证报告文件 | +| `ExportRepositoryDistributionResult` | `distribution_id` | Revision 绑定、目标结果与摘要文件 | +| `MigrateRepository` | `repository_id,target_storage_node_id` | 迁移 Task,复用 manifest 和受控发布 | + +Inventory 兼容 v1/v2 输入;导入要求重新选择目标节点/Key,不携带 Bootstrap Token、私钥、数据库/TMS/OCAI 凭据。配置导入不等于备份恢复。分发目的节点由已有 TMS 注册和心跳产生,Storage Node 注册与 Agent 接入是不同操作。 + +### 4.9 Node 内部 RPC 与 Bootstrap + +协议源为 `manager/backend/protocol/repositorynode/repository_node.proto`。 + +| 服务/RPC | 身份和关键字段 | 职责 | +| --- | --- | --- | +| `RepositoryNodeInternalService.HeartbeatNode` | mTLS;node ID、容量/inode、OS/架构/版本、Repository Usage、Signing Key Inventory | 更新节点观测快照 | +| `ClaimTask` / `HeartbeatTask` | mTLS;node/worker、lease、Task、fencing token | 按节点领取、续租、报告进度和接收取消 | +| `ReportTaskFailure` | mTLS;Task/node/worker/fencing、状态、错误、可选结果对象与隔离数 | 不发布失败 Revision,结束失败/取消任务 | +| `PrepareTaskCommit` / `ConfirmTaskCommit` | mTLS;Task/node/worker/fencing、revision/manifest/result object SHA、commit token | 分阶段导入结果并确认节点原子发布 | +| `ExecuteDistributionTask` | mTLS;绑定的 Task 与 fencing | 管理面复用现有 TMS 执行分发,不向 Worker 下发 TMS 凭据 | +| `RenewNodeCertificate` | 已建立的 mTLS;node ID、CSR | 校验节点状态与续期窗口后更新证书 | +| `RepositoryNodeBootstrapService.BootstrapNode` | 独立 TLS;`enrollment_id,install_task_id,tms_agent_id,bootstrap_token,csr_pem,authorization_id,architecture` | 首次身份授权或精确请求恢复;响应为 node ID、公开证书/CA、serial/fingerprint 和有效期 | + +Bootstrap 是尚未建立客户端证书时的专用入口,不要求已有 mTLS。授权事务再次检查 Allowlist、Enrollment、Task、Agent、架构、Token 和 CSR,原子消费 Token;事务外签发证书。普通重放返回 `REPOSITORY_NODE_TOKEN_REPLAY`;相同持久化 authorization/CSR/身份恢复不得重新消费 Token。已注册时返回原证书。Node 禁用/删除后内部 heartbeat、task polling 等仍由正式节点状态检查拒绝;禁用 Allowlist 只限制新的注册,不自动中断已上线节点。 + +### 4.10 Node HTTPS 数据面和公开发布 + +| 方法与路径 | 功能 | +| --- | --- | +| `GET/HEAD /internal/v1/probe` | mTLS 节点探测 | +| `GET/HEAD /internal/v1/task-results/{task_id}` | 核对结果对象 | +| `GET/HEAD /internal/v1/task-logs/{task_id}/metadata`、`.../content` | 日志索引与压缩内容 | +| `POST /internal/v1/uploads/{upload_id}/begin`、`.../complete` | 创建/合并上传对象 | +| `PUT /internal/v1/uploads/{upload_id}/content` | 旧 offset 顺序分片兼容 | +| `PUT /internal/v1/uploads/{upload_id}/chunks/{chunk_index}` | Indexed Chunk 节点落盘 | +| `DELETE /internal/v1/uploads/{upload_id}` | 清理上传对象 | +| `PUT/HEAD/DELETE /internal/v1/gpg-keys/{repository_id}/{fingerprint}` | 公钥同步/检查/删除 | + +上述接口使用节点控制地址的 mTLS 服务,并校验路径、大小和摘要。公共发布地址由 `publish_base_url` 与 `publish_key` 生成: + +```text +GET/HEAD //current/... +GET/HEAD //revisions//... +``` + +发布层 volume 只读、关闭 autoindex,拒绝 POST/PUT/PATCH/DELETE。上传必须走 JWT 管理 API,不能写公共目录。 + +### 4.11 错误目录与排障信息 + +现有 API 同时保留英文 `error_code/error_message`,Task、Target、Node 等响应增加统一中文 `error_zh_message/error_zh_recommended_action`。权威目录为 `manager/backend/services/footstone/domain/repositoryErrorCatalog/catalog.go`。 + +代表类别包括 Node 离线/未授权、预检过期/空间不足/inode/工具、同步 DNS/HTTP/元数据/摘要、上传损坏/架构/签名/NEVRA、DNF 包/依赖/GPG、日志过期及 Package Install/分发失败。错误别名由统一目录处理,页面不另建翻译表。日志保留原始排障信息并统一脱敏;未知 Code 使用中文兜底,不能被吞掉或替换为普通 Success。 + +## 5. 前端设计 + +### 5.1 页面结构与技术边界 + +控制台继续使用 Vue 3、Vue Router、Pinia、TDesign Vue Next 和现有 HTTP 封装,没有新增 UI 框架或独立任务系统。“软件仓库”下设仓库管理、任务总览、仓库节点、上游源四个一级入口。 + +仓库详情固定为五个页签:**概览、软件包、任务、节点分发、仓库配置**。上传通过软件包页的 Drawer 完成;隔离区与软件包清单在同页切换;GPG 公钥、策略与 Repo 配置放在仓库配置页。不再使用早期八页签布局。 + +本报告嵌入的界面截图统一改为 2026-10-24 当前部署的真实页面,覆盖最新查询区、表格、紧凑状态标签、节点运维及各操作入口。已完成任务的结果图由当前界面读取既有记录;预览截图未确认执行。图中的节点名称、地址、任务 ID、容量和 RPM 版本是实验数据,不是部署默认值。历史实验的文字、数值与原始证据仍保留,重拍页面不冒充该日期重新执行。页面展示不是功能测试的替代品,具体结果仍需结合 API、命令和验收证据。 + +当前截图文件、观察范围与 SHA-256 见[2026-10-24 当前页面截图清单](evidence/repository-documentation-screenshots-20261024.json);[2026-10-22 交互取证](evidence/repository-ui-captures-20261022.json)和[2026-10-15 原始清单](evidence/repository-ui-captures-20261015.json)继续作为历史证据保留。当前上传队列图是尚未选择本地文件的真实入口,不代表已完成传输;发布与恢复结果仍由对应任务、Session/Chunk 和命令证据证明。OCAI 输出为建议,不替代 RPM 校验结论。 + +### 5.2 仓库列表、筛选与批量配置 + +主列表采用服务器分页平铺表格。系统/产品、版本、仓库类型、架构和关键词共同过滤结果,不再把当前页二次分组。默认把系统/版本、类型/架构、包数/内容大小分别合并展示,减少横向宽度;可通过“显示列”齿轮按钮切换完整字段、选择显示列或恢复默认。列偏好只保存在当前浏览器,浏览器存储不可用时不影响页面操作。表头和横向滚动条随当前滚动容器保持可达,宽表不必先滚到页面底部再横移。短 ID 的 Tooltip 保留完整值。 + +| 控件 | 操作与结果 | +| --- | --- | +| 创建仓库 | 打开仓库表单,填写基础信息、主仓库节点和安全策略 | +| 显示列 | 选择显示列、切换完整字段或恢复默认,必要身份与操作列保留 | +| 批量 Repo 配置 | 对已选仓库生成配置,可复制或下载 | +| 上游源、仓库节点 | 进入对应配置页面 | +| 导出仓库清单 | 导出 Repository 定义,不导出 RPM、私钥或凭据 | +| 导入仓库定义 | 选择清单、目标节点及映射,Preview 后 Confirm | +| 刷新 | 重新获取当前筛选条件下的数据 | +| 行内查看、编辑、同步、删除 | 查看详情、保存配置、创建原有 Sync Task、按后端保护规则删除 | + +![当前仓库主列表、合并指标与操作入口](images/repository/repository-current-20261024-list.png) + +![显示列:应用、取消、恢复默认与全部字段](images/repository/repository-current-20261024-columns.png) + +### 5.3 上游源与创建表单 + +上游源支持内置 OpenCloudOS 来源、项目自定义地址模板和完全手工 URL。创建表单根据来源的系统版本、channel 和架构展示可用快捷包组与排除规则,同时保留自定义输入。快捷项是方便填写,不代表任意版本上游一定包含某个包;实际结果以节点读取的上游元数据为准。 + +表单包含系统/产品、版本、名称、仓库类型、架构、GPG 包策略、主仓库节点、可用的元数据签名 Key、描述及启用状态。选择远程来源后还可设置 MIRROR/APPEND、下载并发、带宽限制和同步范围。 + +![当前创建表单的基础信息、节点与来源选择](images/repository/repository-current-20261024-create-upstream.png) + +![来源对应过滤规则与下载控制](images/repository/repository-current-20261024-create-filter.png) + +![上游来源列表和适用架构](images/repository/repository-current-20261024-sources.png) + +筛选同步必须明确 Include;选择全量同步会提示容量风险。默认筛选预算为 2 GiB/1000 包,全量预算为 64 GiB/100000 包,并受节点实际容量和管理员硬上限约束。预算不是已探测到的上游精确大小。 + +删除受系统用途、当前发布包、有效未完成上传、GPG 公钥和运行任务保护。隔离项和普通失败任务记录本身不阻止删除。系统管理的 Node RPM 源不能通过普通仓库删除入口随意移除。 + +### 5.4 节点列表与节点运维 Drawer + +节点列表显示名称、IP/Host、架构、在线状态、仓库数量、仓库实际占用、磁盘已用/总量、使用率、版本、心跳和最近错误。顶部统计由后端对完整筛选结果计算,不是前端累加当前分页。 + +“当前内容大小”是 active Revision 的 RPM 逻辑大小;“节点实际占用”包含可归属的历史 Revision、staging、quarantine、metadata、trash 等文件系统分配空间。两者不能当作同一个指标。 + +节点详情继续使用 Drawer,分为存储概览、Repository/Source 列表、最近任务三个区域。每行同步按钮复用现有 Sync API、Mutation Lock、Worker、Revision 和 Atomic Publish。无上游、节点离线、仓库禁用或已有 Sync 时不能再次启动。最近任务直接查询 Global Task API 的节点筛选;“查看全部任务”带节点条件跳转任务总览。 + +![节点列表与完整集合容量统计](images/repository/repository-current-20261024-nodes.png) + +![节点运维 Drawer、仓库指标和同步入口](images/repository/repository-current-20261024-node-drawer.png) + +### 5.5 Node RPM 安装、安装源与注册白名单 + +“手动注册”适合已有节点;“安装仓库节点”提供安装/升级、Node RPM 安装源、注册白名单三个工作区。 + +安装源不是额外的二进制分发通道。Node RPM 先通过普通私有仓的上传、签名校验和发布流程形成标准 YUM/DNF 源,再把该仓绑定为受保护的 `NODE_RPM` 系统用途仓。登记 Artifact 时关联发布 Package、immutable Revision、架构、SHA-256 与构建提交。 + +用户选择目标 TMS Agent 后,后端只返回当前项目、启用安装源、架构匹配的正式 Artifact。“最新”按 RPM EVR 比较,不按上传时间;也允许选历史版本。TMS 下发安装命令与临时 Repo 配置,目标节点从该 immutable 源安装指定 NEVRA,完成本地私钥/CSR 生成和自动注册。安装完毕清理临时 Repo。 + +注册白名单以项目和 TMS Agent ID 为身份,架构可强校验,IP/hostname 是附加约束。创建 Enrollment 和 Bootstrap 授权时都校验白名单;一次性 Token 消费后只有精确匹配已持久化授权的请求才能幂等恢复。白名单撤销阻止新注册,不自动下线已上线 Node;下线使用“禁用节点”。 + +![节点安装入口、目标架构与 RPM 版本参数](images/repository/repository-current-20261024-node-install.png) + +![标准 Node RPM 安装源与 Artifact 登记](images/repository/repository-current-20261024-node-install-sources.png) + +![Agent 注册白名单及附加约束](images/repository/repository-current-20261024-node-allowlist.png) + +### 5.6 概览与计划同步 + +概览显示发布状态、活动 Revision、主仓库节点、稳定发布地址,以及包数、当前内容大小、Revision 数和隔离项数。低频配置折叠显示。Revision 是一次不可变发布快照的标识,不是主机名或软件版本。 + +有上游的仓库提供计划同步:每日/每周、时间、时区、连续失败阈值,以及保存、暂停、恢复操作。界面展示最近/下次运行、连续失败与错误;调度由后端幂等创建任务,已有同步时记录跳过,不额外制造失败任务。 + +![仓库概览与五个详情页签](images/repository/repository-current-20261024-overview.png) + +### 5.7 软件包、安装预览和删除预览 + +软件包表展示 NEVRA 对应版本、架构、签名状态、所属 Revision 和时间,可按包名、架构、签名及未签名条件过滤。 + +“安装到节点”先按每个目标 Agent 生成结构化 Preview,展示选中包、依赖、升级、替换、下载量和磁盘变化,再确认执行。Preview、Confirm 和 Execute 绑定同一 immutable Revision;任务按 Target 独立记录结果,失败目标优先展示。 + +节点选择器支持按 IP/Agent 标识搜索、仅看在线、刷新和已选数量。IP、完整 Agent 标识与状态整行对齐,离线或异常身份禁选;同 IP 的不同 Agent 不合并,安装仍按字符串 Agent ID 精确提交,不转为 IP 或 JavaScript Number。搜索隐藏节点不清除有效选择,刷新后失效的选择会移除并提示。安装预览结果表独立滚动,不把确认/取消按钮挤出弹窗。包来源、发行号、发布版本和常见状态使用中文标签,协议原值与技术详情保留。 + +单包和批量删除都先预览直接/传递反向依赖,分析只使用绑定的 Repository Revision,不使用机器默认系统源。Confirm 校验 Digest、有效期和 Revision。批量删除创建一个后端 Task,保存逐包结果,并只重建一次 repodata,不是浏览器循环调用单包删除。 + +![软件包清单与上传、安装、批量删除入口](images/repository/repository-current-20261024-packages.png) + +![当前安装节点选择、搜索、在线筛选和选择数量](images/repository/repository-current-20261024-package-install-nodes.png) + +![当前受控软件包只读安装预览,未点击确认安装](images/repository/repository-current-20261024-package-install-preview.png) + +多包、多目标的历史真实安装实验仍见第 7.7 节;本次只读预览不能替代真实安装结果。 + +![删除预览与依赖影响](images/repository/repository-current-20261024-batch-delete-preview.png) + +### 5.8 上传队列与隔离区 + +上传 Drawer 支持 RPM 或 tar.gz,多文件分别建立 Upload Session,默认同时传输两个文件。当前使用 4 MiB Indexed Chunk,允许乱序补传;总进度按已上传字节/总字节计算。每个文件有排队、上传中、暂停、完成、失败、取消状态,提供暂停、恢复和取消。 + +网络中断或人工暂停后继续缺失分片;刷新页面后可恢复服务器 Session 元数据,但浏览器无法自动恢复本地 File 时须重新选择同一文件。服务端验证分片与完整文件 SHA-256,完整文件 Hash 不要求在传输前由浏览器全量计算。 + +传输完成后仍要查看异步摄取 Task:上传完成不等于发布成功。隔离区可查看拒绝文件、稳定错误码、原因和关联任务;危险归档、损坏 RPM、错误签名不得进入 active Revision。tar 部分接受显示 `PARTIAL_SUCCESS`,不能冒充普通成功。 + +![当前上传队列入口,尚未选择本地文件](images/repository/repository-current-20261024-upload-queue.png) + +![软件包和隔离区切换](images/repository/repository-current-20261024-quarantine.png) + +### 5.9 任务总览、日志与 OCAI + +全局任务支持按仓库、节点、任务类型、触发方式、状态和创建时间范围查询。顶部四态统计受其他过滤条件影响,但不受当前状态筛选影响;点击统计卡再设置列表状态过滤。时间按 UTC/RFC3339 传输,按浏览器规则展示。 + +任务表默认合并状态/进度,低频时间与触发字段可在“显示列”展开;表头和横向滚动条保持可达,筛选和服务器分页行为不变。 + +只读 Task Drawer 不提供无效的“确认”按钮。详情包含业务任务、目标结果、逐包结果、错误码、中文说明、建议与日志。完整日志由后端提供脱敏 `tar.gz` Bundle,包含上下文、结果摘要、JSONL 日志和 SHA256SUMS,不由前端拼装文本冒充完整日志。 + +OCAI 分析入口在“仓库详情 → 任务 → 任务详情”,不是全局任务摘要抽屉。在当前分析面板中展示加载、流式结果、失败和重试。输入是可预览、脱敏、有界的失败摘要和上下文;输出原因、证据和建议。重试、重新同步或修复仍由用户确认并调用 OCManager API,OCAI 不自动执行管理命令。 + +点击分析后入口显示“正在准备/分析中/查看分析”等反馈;确认上下文后在当前 Drawer 内滚动并聚焦结果区域,不要求用户自己寻找底部结果。同一分析可再次定位,失败可重试;切换任务或关闭详情后,过期异步响应不能写入新任务。发送上下文可展开查看,重复点击不重复创建正在准备的分析,不更改现有 SSE 或模型执行链路。 + +![当前全局任务、合并状态进度和四态统计](images/repository/repository-current-20261024-tasks.png) + +![当前全局失败任务摘要、中文原因和完整日志入口](images/repository/repository-current-20261024-task-failure.png) + +下图从“OCAI 完整页面”查看当前任务刚完成分析后保存的历史会话,展示真实分析内容;不是 Drawer 截图。任务内面板的自动定位行为仍按上文说明,查看完整页面不代表重新执行任务或建议中的命令。 + +![OCAI 完整页面中当前任务已完成分析的历史会话结果](images/repository/repository-current-20261024-ocai-result.png) + +2026-10-22 的历史取证使用已有损坏 RPM 失败任务,真实模型完成只读分析;没有执行建议中的命令、重新上传、重试任务或安装。其原图随历史清单保留,本节现嵌入 2026-10-24 当前页面。共享 Chat 在主动停止或 HTTP 200 后异常 EOF、没有结束事件时,包装状态可能仍停留在“分析中”;不将正常完成截图扩大为全部断流恢复已通过。关闭/刷新后也不承诺自动恢复整段本地分析状态。 + +### 5.10 仓库配置与 GPG + +进入 Repo 配置即生成真实配置,可切换稳定地址或 immutable Revision,复制、下载或重新生成。选中多个仓库也可批量生成。E2E 使用相同产品接口生成的配置,不另写一套绕过产品的源文件。 + +GPG 工作区管理公钥和包策略:DISABLED、AUDIT、ENFORCE,兼容旧 VERIFY_PACKAGE 的严格校验语义。AUDIT 记录签名异常,ENFORCE 拒绝不合策略的包。Metadata Signing Key 由节点本地提供,管理面只引用 key_ref 和公开指纹。只有签名、公钥、发布文件完整时,生成器才允许输出 `repo_gpgcheck=1`;切换签名 Key 不改历史 immutable Revision。 + +![动态 Repo 配置与复制、下载](images/repository/repository-current-20261024-config.png) + +![GPG 公钥、包策略与元数据签名](images/repository/repository-current-20261024-gpg.png) + +### 5.11 节点分发与清单导入 + +节点分发绑定 repository_id、revision_id 和 manifest SHA-256。目标列表来自 TMS Agent,不来自仓库节点注册表;同一机器可以兼任角色,但必须分别建立身份。分发执行下载到 staging、校验、原子切换,结果按目标展示并可导出。 + +分发列表直接显示该历史记录的目标 IP;前两个目标横向排列,间距固定,更多目标通过“另 N 个”查看完整列表。各列均设置明确宽度,避免目标节点列独占剩余空白。详情使用中文标签,保留原始日志、完整标识、校验值和逐目标状态;未结束目标的默认退出码不显示为成功。当前创建 API 按 IP 选择目标,同 IP 多身份或连接状态冲突时禁选并提示确认 TMS 注册信息,不靠排序猜测 Agent、不自动清理历史记录。 + +![当前分发列表、横向目标 IP 与均衡列宽](images/repository/repository-current-20261024-distribution.png) + +![当前分发详情中文标签与历史逐目标结果](images/repository/repository-current-20261024-distribution-detail.png) + +清单导入使用 Preview/Confirm,显式选择目标存储节点及签名 Key 映射。Confirm 创建统一 `INVENTORY_IMPORT` Task;Job 只保存导入特有结果。导入恢复的是定义,不是 RPM、Revision、Task History 或 Secret 的备份恢复。 + +![仓库定义导入预览](images/repository/repository-current-20261024-inventory-import-preview.png) + +### 5.12 登录反馈与前端回归 + +登录按钮显示处理中状态并防止重复提交。IdentifyAccount 的密码错误、限流或锁定响应保留后台原因,不再误跳资源权限 403 页;其他业务接口的认证/授权规则保持不变,不修改密码、锁定策略或服务端权限。 + +`npm run test:repository` 同时包含 Repository、相关 HTTP 登录错误分支与登录表单回归。Repository 类型检查保留无关模块的原始诊断,并严格识别 Repository 文件路径;不关闭规则或隐藏本次错误以通过检查。该前端增量不改变数据库、RPC、Worker、Task、Revision、Atomic Publish、TMS 或 OCAI 主链。 + +本次代码基线 `859648f` 的前端门禁为 **16 文件、105/105 PASS**,定向 ESLint/Stylelint、Repository vue-tsc、正式构建均通过。既有 chunk-size 提示和其它模块类型诊断保留,未声称全项目零告警;详细命令和交互复现见[测试文档 1.4](私有化软件仓库测试验收.md#14-控制台交互增量复现)。本次文档补充不改业务源码,不更新历史 152 项结果文件。 + +## 6. 部署与配置 + +### 6.1 运行依赖 + +推荐使用 OpenCloudOS 9。Repository Node 需要: + +```text +dnf-plugins-core +createrepo_c +rpm / rpmkeys +rpm-sign +gnupg2 +zstd +Docker / Docker Compose +``` + +前端依赖保持上游已有的 Stylelint 16.14.1,不另行引入 UI、CSS 或状态管理框架。当前锁文件在 Node.js 24.15.0、npm 10.9.4 下完成复现;由于现有 `jsdom` 和 `vitest` 的运行要求,开发与验收环境建议使用 Node.js 20.19+、22.13+ 或 24+,安装时继续遵循项目已有 `.npmrc`。 + +```bash +cd manager/frontend +npm ci +npm run typecheck:repository +npm run stylelint +npm run test:repository +npm run build:test +``` + +### 6.2 关键端口 + +| 端口 | 组件 | 策略 | +| --- | --- | --- | +| OCManager 现有管理端口 | Vue/Nginx/footstone | JWT | +| `19443` | Repository Node 内部控制与数据接口 | HTTPS + mTLS | +| `19444` | 首次 Bootstrap 管理入口 | TLS + 一次性授权,按需启用 | +| `18090` | Repository Publisher | 隔离网内只读 `GET/HEAD` | +| TMS 现有端口 | Agent 任务链路 | 复用现有部署 | + +### 6.3 关键配置 + +| 配置 | 默认值/说明 | +| --- | --- | +| `REPOSITORY_DATA_HOST_DIR` | `/opt/ocmanager/repository-data` | +| `REPOSITORY_DATA_ROOT` | `/var/lib/ocmanager/repository-data` | +| `REPOSITORY_NODE_INTERNAL_PORT` | `19443` | +| `REPOSITORY_PUBLIC_PORT` | `18090` | +| `REPOSITORY_NODE_HEARTBEAT_SECONDS` | `10` | +| `REPOSITORY_MAX_UPLOAD_BYTES` | 2 GiB | +| `REPOSITORY_MAX_CHUNK_BYTES` | 4 MiB | +| `REPOSITORY_MAX_ARCHIVE_FILES` | 1000 | +| `REPOSITORY_MAX_EXPANDED_BYTES` | 8 GiB | +| `REPOSITORY_MAX_SYNC_BYTES` | 全量同步及所有同步的硬上限,默认 64 GiB | +| `REPOSITORY_MAX_SYNC_PACKAGES` | 全量同步及所有同步的硬上限,默认 100000 | +| `REPOSITORY_FILTERED_SYNC_BYTES` | Include 非空的筛选同步预算,默认 2 GiB | +| `REPOSITORY_FILTERED_SYNC_PACKAGES` | Include 非空的筛选同步预算,默认 1000 | +| `REPOSITORY_METADATA_RESERVE_BYTES` | 512 MiB | +| `REPOSITORY_MINIMUM_FREE_BYTES` | 1 GiB | +| `REPOSITORY_WORKER_LEASE_SECONDS` | 60 | + +证书目录使用 `/etc/ocmanager/repository-node-pki`,只读挂载到容器。私钥不能位于 Git、MySQL、Evidence 或 Repository public 目录。自动注册还需管理面启用 Bootstrap 与受保护的 CA 签发配置;Node Metadata Signing 使用单独的节点本地受限 Key 目录。完整变量、SAN 与权限设置见部署说明,不在本节复制另一套默认配置。 + +### 6.4 Compose 启动示例 + +管理面从源码根目录按照部署说明准备私有 `config/.env`、数据库及 TMS Secret 后执行: + +```bash +YES=1 bash scripts/deploy.sh +``` + +Repository Node 的源码 Compose 路线用于首节点或手动部署;后续节点也可使用第 7.1 节的 RPM/TMS 安装。先完成静态 Worker 构建和 PKI 准备,以下占位值必须按当前环境替换: + +```bash +export REPOSITORY_DATA_HOST_DIR=/opt/ocmanager/repository-data +export REPOSITORY_NODE_PKI_HOST_DIR=/etc/ocmanager/repository-node-pki +export REPOSITORY_NODE_ID= +export REPOSITORY_NODE_CONTROL_TARGET=ip://:19443 +export REPOSITORY_NODE_SERVER_NAME= +export REPOSITORY_PUBLIC_BASE_URL=http://:18090/repositories + +make -C manager/backend build-bin-static +docker compose \ + -f manager/deployments/compose/repository-node.compose.yml \ + up -d --build +``` + +启动后检查: + +```bash +docker compose -f manager/deployments/compose/repository-node.compose.yml ps +curl -I http://:18090/health +curl -I http://:18090/repositories//current/repodata/repomd.xml +``` + +管理面内部端口与 Node 数据端口是两条服务;同机部署不能同时绑定相同宿主端口。自动注册默认还使用 `19444`,同机数据端口示例因此采用 `19445`。具体命令、端口映射、首节点证书和升级/恢复顺序以[部署说明](私有化软件仓库部署说明.md)为唯一操作依据。本节仅作关系概览,不是省略 PKI/配置步骤的一键安装承诺。 + +## 7. 使用说明与示例 + +以下按当前控制台操作顺序展开。前提是按[部署说明](私有化软件仓库部署说明.md)启动管理面,并至少有一个在线 Repository Node;需要远程安装或分发时,还必须有在线 TMS Agent。OCAI 是独立可选组件,不部署时不影响普通同步、上传与发布,但不能宣称真实 AI 分析已经验收。 + +### 7.1 准备仓库节点与安装源 + +进入“软件仓库 → 仓库节点”,检查状态、磁盘、inode、版本和最近错误。对现有节点点击“立即探测”,再打开“节点运维”查看容量和该节点承载的 Repository。 + +![步骤 1:统一查询区的仓库节点列表](images/repository/repository-current-20261024-nodes.png) + +![节点运维:紧凑在线标签、Repository 占用和最近任务](images/repository/repository-current-20261024-node-drawer.png) + +第一个节点可按部署文档手工安装并注册。后续从页面安装时,先完成一次初始化: + +1. 创建允许上传的普通私有仓,把构建的 `ocmanager-repository-node` RPM 上传并发布。x86_64/aarch64 分别使用对应架构的包与安装源。 +2. 打开“安装仓库节点 → Node RPM 安装源”,选择已发布的私有仓并保存,再登记包与构建提交。该仓转为受保护的系统用途源。 +3. 在“注册白名单”授权目标 TMS Agent。Agent 是强身份,配置过期时间和架构时须与目标一致。 +4. 回到安装页,选择目标 Agent、默认最新或指定历史 RPM,填写节点控制/发布地址与 TLS 名称,再执行 TMS 安装。 +5. 在任务总览查看 Target 结果,确认新节点上线。TMS 添加临时源并从 immutable Revision 安装 RPM,RPM 启动后自动完成授权注册,不需要手工拷贝 RPM 到每台机器。 + +![步骤 2:选择 Agent、Node RPM 版本并安装](images/repository/repository-current-20261024-node-install.png) + +![先配置 Node RPM 标准安装源并登记已发布包](images/repository/repository-current-20261024-node-install-sources.png) + +![安装前授权目标 Agent](images/repository/repository-current-20261024-node-allowlist.png) + +完整安装参数、安装源和白名单的授权边界见第 4、5 章及部署文档。 + +### 7.2 创建私有仓与筛选同步仓 + +在“仓库管理”点击“创建仓库”。先填写系统/产品与版本,再填写名称、架构、类型和主仓库节点。只上传业务包时选择私有仓;需要发行版源时选择基线/更新仓并配置上游。 + +上游可以选择内置 OpenCloudOS BaseOS/AppStream,也可以新增项目来源模板或手工填写 HTTP(S) URL。选好来源、版本、架构后,再选择该来源对应的常用包组;自定义 Include/Exclude 仍保留。不要把 AppStream 包组套到只含 BaseOS 元数据的地址上。 + +建议第一次只同步少量明确存在的包,例如 OC9 BaseOS 的 `bash`,确认发布后再扩大范围。全量同步必须显式选择,并先确认容量预算,不应为了演示直接下载整个发行版。安全策略按是否有正确公钥选择;严格包签名校验须先导入授权公钥。 + +当前创建表单支持来源选择、筛选/全量、MIRROR/APPEND、并发/带宽及元数据签名 Key。 + +![选择仓库类型、架构、节点和上游来源](images/repository/repository-current-20261024-create-upstream.png) + +![选择匹配来源的包组,同时保留自定义输入](images/repository/repository-current-20261024-create-filter.png) + +需新增来源时,进入“上游源 → 添加上游源”,填写通道、支持架构、URL 模板和默认规则。模板支持 `{version}`、`{architecture}`,也可填固定内网 HTTP(S) 地址。保存的是可复用配置,不会随模板变动静默修改已有仓库。 + +![添加可复用的项目来源模板](images/repository/repository-current-20261024-source-form.png) + +### 7.3 检查概览、执行同步与计划同步 + +创建后打开仓库详情。在“概览”检查节点名称、发布状态、活动 Revision、包数和内容大小。空仓没有活动 Revision 属于正常状态。 + +![步骤 3:检查概览和发布信息](images/repository/repository-current-20261024-overview.png) + +对已配置上游的仓库点击同步,或从“仓库节点 → 节点运维”的 Repository 行点击同步。任务创建后到任务页查看进度;成功后应出现新 Revision、包清单和可读取的 repodata。已有 Sync Running 时不要重复提交。 + +![步骤 4:从节点视角同步并观察新 Revision](images/repository/repository-current-20261024-node-drawer.png) + +需要周期执行时,在概览的计划同步区域设置每日/每周、时区和失败阈值,保存后核对 `next_run_at`。暂停/恢复只改变计划,不等于取消已在运行的 Task。连续失败达到阈值会自动暂停;已有同步任务导致的跳过不计作失败。 + +### 7.4 上传真实 RPM 或多个文件 + +真实包演示可选择与仓库架构匹配的 OpenCloudOS RPM;下载地址必须来自该版本的真实上游元数据。第 8.5 节保留了 20 个官方 RPM 的实测清单和大小结果,可用于选择小、中、大样本,但旧实验文件地址不作为当前部署固定路径。 + +进入私有仓“软件包”,点击“上传软件包”,选择单个 RPM、多个 RPM 或 tar.gz。每个文件独立显示进度与状态,可暂停、恢复、取消。刷新后若文件不可访问,重新选同一文件即可匹配 Session 并补传,不需要从零上传。 + +![步骤 5:打开当前上传队列,选择本地 RPM 或归档文件](images/repository/repository-current-20261024-upload-queue.png) + +传输完成后进入“任务”,确认 UPLOAD Task 成功,再返回软件包清单核对名称、版本、架构、签名、Revision 和数量。只有任务完成发布才代表包已进入可用源。 + +![步骤 6:已签名包、统一查询区和独立批量操作栏](images/repository/repository-current-20261024-packages.png) + +### 7.5 损坏包、隔离区与失败分析 + +在终端复制一个测试 RPM 后截断副本,再上传到同一测试仓。不要破坏原始样本,也不要用文本文件伪装成“已签名但损坏”的证据。比如已设置 `RPM_FILE` 指向合法样本时: + +```bash +mkdir -p .run/repository-demo/input +head -c 128 "${RPM_FILE}" > .run/repository-demo/input/damaged.rpm +``` + +失败后切换“软件包 → 隔离区”,核对拒绝原因与关联 Task,并确认 active Revision 没变。tar 部分合法时,应同时显示已接受包与拒绝项,任务状态为 `PARTIAL_SUCCESS`。 + +![步骤 7:当前隔离区入口;所示仓库此时无隔离项](images/repository/repository-current-20261024-quarantine.png) + +先在全局任务摘要或仓库内任务详情查看英文 Error Code、中文原因、建议与日志。需要 AI 时,从“仓库详情 → 任务 → 任务详情”点击“OCAI 分析”,预览脱敏上下文后点“开始分析”。页面在当前 Drawer 自动定位到分析面板,入口反馈准备/分析中/查看分析;“查看分析”只定位已有会话,重试另行触发。真实模型不可用时明确报错,规则诊断仍保留。OCAI 不自动重跑任务;采用建议后由用户重新上传或调用重试,并关联操作结果。 + +![步骤 8:当前全局失败任务与脱敏日志入口](images/repository/repository-current-20261024-task-failure.png) + +![从仓库内任务详情预览发送给 OCAI 的脱敏上下文](images/repository/repository-current-20261024-ocai-context.png) + +分析完成后也可从“OCAI 完整页面”查看保存的会话。下图展示该当前任务的真实分析结果,不是 Drawer 自动定位画面,也没有执行建议中的操作。 + +![在 OCAI 完整页面查看当前任务分析结果](images/repository/repository-current-20261024-ocai-result.png) + +### 7.6 包级 GPG、元数据签名与 Repo 配置 + +在“仓库配置 → GPG 公钥与策略”导入用于校验 RPM 的**公钥**。公钥来自包的签名者,不是随便在客户端生成一把 Key;演示自建 RPM 时,构建/签名环境生成 Key 并签名,只把公钥交给 OCManager,私钥不上传。 + +![步骤 9:检查 GPG 公钥与策略](images/repository/repository-current-20261024-gpg.png) + +正向验证上传授权 Key 签名的 RPM 并发布;负向上传未授权、错误或不满足策略的样本,ENFORCE 下应隔离且 active 不变。AUDIT 用于记录异常,不等同严格拒绝。 + +需要元数据签名时,先在仓库节点本地配置 Signing Key 并上报 Inventory,再在仓库表单选 `key_ref`,发布新 Revision。公开 `repomd.xml`、`.asc` 和公钥都可用后,生成的 `.repo` 才应包含 `repo_gpgcheck=1`。客户端不能通过关闭校验来掩盖缺少公钥或签名文件。 + +进入“Repo 配置”,按需选择稳定地址或 immutable Revision,复制或下载。安装演示与复现测试应直接使用此内容,不手写另一份不同 baseurl 的配置。 + +![步骤 10:生成、复制和下载真实 Repo 配置](images/repository/repository-current-20261024-config.png) + +在 OC9 客户端,把下载的配置保存到自己创建的测试目录,再使用现有安装脚本。以下命令从 Repository Root 执行,下载文件位置由用户指定: + +```bash +read -r -p '产品下载的 .repo 文件路径: ' repo_file +read -r -p '该仓库中已发布的测试包名: ' package_name +bash scripts/repository-gpg-install-demo.sh \ + --repo-file "${repo_file}" \ + --package "${package_name}" \ + --demo-root "${PWD}/.run/repository-demo" +``` + +修复候选脚本支持单个或多个空格分隔的 `gpgkey` URL,需要 OC9 的 dnf/repoquery、rpm、curl、gpg、jq、awk 和基础 Shell 工具及 root/sudo 权限。它逐个下载公钥,在全新 installroot 中准备元数据信任,禁用默认源后真实安装,保留 Repo ID/baseurl、公钥指纹、来源查询、DNF 退出码和 `rpm -q`。包及其依赖须由指定仓库满足,不能靠宿主默认源补包;启用 Metadata Signing 时保留 `repo_gpgcheck=1`。无 GPG 和 Case A/B/C 使用[测试验收文档](私有化软件仓库测试验收.md)对应脚本。 + +### 7.7 多节点安装与批量删除 + +在软件包清单勾选包,点击“安装到节点”,选择目标 Agent。检查每个 Target 的依赖/升级/替换和容量 Preview,仅 READY Target 可确认。确认后查看统一 Task 下的目标结果,不是每个节点各建一条不相关业务任务。 + +当前选择器按 IP/Agent 搜索,可只看在线、刷新并查看已选数量;离线/异常节点禁选。同 IP 的不同 Agent 保留完整标识,安装按 Agent ID 执行。预览表较宽时在表格内横移,底部按钮仍可达。 + +![步骤 11:当前安装目标选择器](images/repository/repository-current-20261024-package-install-nodes.png) + +![当前只读安装 Preview,未确认实际安装](images/repository/repository-current-20261024-package-install-preview.png) + +以下为 2026-10-24 当前界面的只读预览及读取已有多目标安装结果。预览没有点击“确认安装”,结果图不表示本次截图重新安装。2026-10-14 的原始实验与证据继续保留在历史记录中: + +![当前软件包安装 Preview,未确认执行](images/repository/repository-current-20261024-package-install-preview.png) + +![步骤 12:当前界面读取已有任务的逐 Target 安装结果](images/repository/repository-current-20261024-package-install-result.png) + +删除时勾选包,点击“批量删除”,查看直接和传递依赖影响。默认不级联删除;确认后的 Digest/Revision 若失效,应重新 Preview。最终在任务详情核对逐包结果与单次 Revision 更新。 + +![步骤 13:批量删除 Preview](images/repository/repository-current-20261024-batch-delete-preview.png) + +![步骤 14:同一 Task 下的逐包删除结果](images/repository/repository-current-20261024-batch-delete-result.png) + +普通仓库删除前须无当前发布包、有效未完上传、GPG 公钥和运行任务。先删包再删除未使用的公钥;历史 Revision、隔离项和普通 FAILED Task 不作为永久阻塞。Node RPM 系统源有额外用途保护,不能用普通删除操作绕过。 + +### 7.8 不可变 Revision 分发 + +进入“节点分发”,创建分发任务并选择在线 TMS Agent。仓库节点与分发目标不是同一张注册表:新增目标机器先安装/接入现有 Agent,使其在 TMS 在线;创建 Storage Node 不会自动生成 Agent。 + +分发始终绑定当时的 Revision 和 manifest,中央仓库随后发布新版本不改变本次任务内容。完成后检查目标结果、SHA-256 和原子切换,不把文件已下载等同于全部目标成功。 + +同 IP 存在多个身份或状态冲突时,创建选项会禁选并提示核对有效 TMS 注册;不要删除未知历史记录或伪造在线状态绕过。下图是当前界面读取历史已完成记录,没有因此创建新分发任务。 + +![当前界面读取已完成的双目标分发,IP 横向紧凑展示](images/repository/repository-current-20261024-distribution.png) + +![历史分发的完整版本、校验值与逐节点输出](images/repository/repository-current-20261024-distribution-detail.png) + +本节分发复用原有 TMS 链路;Node RPM 安装则额外经过系统安装源、版本选择与 Enrollment 白名单,不应把两种操作混为一谈。 + +### 7.9 清单导入、任务检索与导出 + +导出清单后,在“导入仓库定义”选择目标节点、冲突策略及 Key 映射,先 Preview 再 Confirm。导入只恢复定义,不含 RPM 内容、私钥、Token 或 Task History。 + +![步骤 15:检查定义导入映射和冲突结果](images/repository/repository-current-20261024-inventory-import-preview.png) + +确认后到任务总览查看 `INVENTORY_IMPORT`。可从节点“查看全部任务”带节点过滤进入,也可直接按仓库、触发方式、类型和创建时间筛选。列表与四态统计使用一致的时间字段,失败筛选不会让其他统计卡变成零。 + +![步骤 16:统一查询区的全局任务检索和统计](images/repository/repository-current-20261024-tasks.png) + +任务详情可导出诊断及后端脱敏日志 Bundle,分发结果和验证报告也有独立导出接口。长期保存时同步保存源码版本、结果 JSON 与文件 SHA-256,避免只保留一张成功截图。 + +### 7.10 API 调用和自动化入口 + +管理接口与字段见第 4 章;调用时使用登录接口返回的 `jwt_key` 和 JWT,而不是固定 Header 或文档默认密码。将实际管理面地址、项目、节点与仓库 ID 作为部署参数,不把截图中的地址复制成产品配置。 + +批量定义走 `BatchApplyRepositories`,上传走 `Begin → Put indexed chunks → Complete → Task`,删除/安装/导入走 Preview/Confirm。所有自动化都调用产品 API,不直接写 MySQL 或创建发布目录。 + +| 工具/入口 | 用途与前提 | +| --- | --- | +| `bash scripts/check-environment.sh` | OC9 环境/工具/容量检查;安装系统工具前确认 sudo 和镜像网络 | +| `YES=1 bash scripts/deploy.sh` | 按部署文档准备配置后部署;不能替代 Node PKI 和可选组件配置 | +| `BatchApplyRepositories` API | 批量定义仓库的产品入口;先提供 JWT、项目、目标节点和配置清单,无直接写库脚本 | +| `make -C manager repo-e2e` | 受控同步、上传与隔离安装场景;依赖可用管理面/节点和测试 RPM | +| `make -C manager repo-report` | 验证产品 inventory/validation/distribution 导出;运行前按测试文档准备配置与测试环境 | +| `bash scripts/repository-gpg-install-demo.sh --help` | 查看真实 GPG 安装演示参数;不是构建/签名 Key 的入口 | +| `make -C manager repo-acceptance` | 按测试文档配置执行核心流程、恢复及最终结果校验;缺少所需组件/证据会非零退出 | + +最终 152 项的预期集合由 `manager/acceptance/repository-acceptance-criteria.json` 固定定义,结果必须逐项关联 implementation/tests/evidence。总入口不会凭空生成缺少的人工和阶段结果;准备方式、环境变量、单/多 Agent 以及结果文件前提见[测试验收](私有化软件仓库测试验收.md)。 + +## 8. 验收结果 + +本章保留有代表性的真实 RPM、容量、恢复与故障实验。Build100~Build154 的日期、数据和命令属于历史记录;原始旧图继续随历史证据保存。为避免旧布局混入使用说明,本章嵌入的操作图片也改用 2026-10-24 当前页面,图片不替代历史实验的原始执行证据。当前产品操作使用第 5、7 章,最终候选 152 项与结果口径见第 11 章。历史 `.run` 路径仅是原始取证定位,不是部署命令或唯一公开结果入口。 + +### 8.1 Build100 基线与 Build106 全量复测 + +2026-09-04 在 OpenCloudOS 9 新拓扑上执行完整验收: + +```text +Repository Acceptance + +31/31 PASS +0 failed +0 blocked +0 pending +Release Acceptance: PASS +final_ready: true +``` + +对外 9 类结果: + +| 分组 | 分类 | 结果 | +| --- | --- | --- | +| Core | Repository Management | PASS | +| Core | Package Ingestion | PASS | +| Core | Repository Publishing | PASS | +| Core | DNF Validation | PASS | +| Core | Automation | PASS | +| Core | Security | PASS | +| Enhanced | Distribution | PASS | +| Enhanced | Diagnosis | PASS | +| Enhanced | Repo Generator | PASS | + +内部仍保留 AC-01 至 AC-31 细粒度矩阵,覆盖 CRUD、批量、多通道灌入、quarantine、repodata、原子发布、GPG、任务锁、三类 DNF、报告、诊断、分发、部署、恢复和文档。 + +2026-09-08 在虚拟机整机重启和网络恢复后,又从本地主仓创建带 SHA-256 的 Build106 版本化源码快照,并执行完整 `repo-acceptance`。本轮不是只读取旧矩阵:该 Build 源码重新完成后端回归、前端 6 个文件 26 个用例与正式构建、隔离 DNF、报告导出、32 类规则诊断、1 GiB 日志、真实 OCAI SSE、GPG、immutable Revision 分发以及恢复后再次验收。最终仍为 `31/31 PASS`,9 个对外分类全部 PASS,`final_ready=true`。 + +本轮首次构建 recovery Worker 镜像时,当时配置的内网镜像地址在管理节点无法解析;Go 二进制和 Footstone 镜像已成功构建,失败只发生在获取 OC9 基础镜像元数据。随后按恢复脚本提供的显式复用开关,使用 2026-09-04 已通过完整恢复验收的本地固定镜像完成 disposable restart/redeploy/destroy-restore;当前源码自身仍由本轮 Go/前端测试覆盖。干净环境如要求重新构建 Worker 镜像,需先确保官方固定 OC9 镜像或导入内网的等价镜像可达。 + +Build103 针对当时的 MariaDB 验证环境补充了节点查询和分发结果查询的兼容修复。聚焦 Go 回归测试均通过,运行镜像健康;修复后在同一套页面完成节点探测、官方 RPM 上传、新 Revision 发布、OCAI SSE 分析和向独立 Agent 目标的实际分发。该增量复测用于证明本文第 7.1 节的操作链路,不替换 Build100 已归档的 31 项全量 Evidence。 + +### 8.2 上游源产品化回归(Build104) + +Build104 在不改变 Repository 同步字段和 worker 数据面的前提下,增加了 OpenCloudOS 内置来源、项目上游源 CRUD、快捷/自定义地址切换以及 include/exclude 快捷配置。验证结果如下: + +| 检查 | 结果 | +| --- | --- | +| 后端 Repository Profile PO、Domain Service、Application、Presentation 测试 | PASS | +| OC9 环境 `go build ./...` | PASS | +| 前端 Repository 专项测试 | 6 个文件、26 个用例全部 PASS | +| 前端 Repository 专项检查与 `vite build --mode test` | PASS | +| OpenCloudOS BaseOS/AppStream `repomd.xml` 可达性 | HTTP 200 | +| 实际 API create/update/describe/list/delete | PASS,测试数据已清理 | +| 原有 Repository 与 Storage Node 查询回归 | 5 个仓库、2 个节点,PASS | +| 生产静态资源与 Footstone health | HTTP 200,PASS | + +后端镜像为 `oc-footstone:20260907-build104-upstream-profile`;前端使用独立版本化快照只读挂载,压缩包 SHA-256 为 `2eadd7de164b64de29dd07dc5a626cc3632a4182ffa1abfc4504298b750628bc`。旧镜像、旧前端目录和 Compose 链均保留,可单独回退。本次增量不会把来源配置的后续变更传播到已有仓库,因此不改变已归档的 RPM、Revision、DNF 和分发 Evidence。 + +### 8.3 前端质量工具与验收边界回归(Build105) + +Build105 保留上游使用的 Stylelint 16.14.1,删除与 Stylelint 16 不兼容且已经失去作用的 `stylelint-config-prettier`、`stylelint-less`,改用 `postcss-html` 和 `postcss-less` 解析 Vue 与 Less。`stylelint.config.js` 同步改为项目 `type: module` 所要求的 ESM 配置。修改没有引入新的 UI framework、CSS framework、状态管理或 HTTP client。 + +| 检查 | 结果 | +| --- | --- | +| npm 10.9.4 隔离目录执行 `npm ci` | PASS,1177 packages | +| `npm run stylelint` | PASS,无已移除规则或 Vue 解析错误 | +| `npm run test:repository` | PASS,6 个文件、26 个用例 | +| `npm run build:test` | PASS,6432 modules transformed | +| 依赖锁文件变更 | 仅围绕 Stylelint 配置解析依赖收敛,未升级 UI 技术栈 | +| Build105 版本化前端快照 | SHA-256 `bea695d0f7506281bc6dae79ef448293eb508f19a61bac03277775f001021a70`,HTTP 200 | +| Build105 Repository 页面实拍 | 仓库列表、上游源、创建表单、概览、任务、上下文预览和 OCAI 会话均可打开 | +| OCAI 直接/代理健康检查 | HTTP 200 | +| Build105 DeepSeek SSE 在线复测 | PASS:HTTP 200,`done=1`,`error=0`,响应 71,481 B,耗时 19,642 ms | + +运行环境将该版本源码构建生成的 `manager/frontend/dist` 只读挂载到前端 Nginx。上一版本的静态目录与 Compose 链保留,可独立回退;此次仅替换前端静态资源,没有改动 MySQL、Repository Node 数据或 active Revision。 + +老师确认的验收边界已经落实到实现和页面口径: + +- OCAI 负责读取脱敏失败摘要、判断原因并给出建议,不自动改仓或执行任务;页面明确使用“由 OCManager 重试”,实际操作继续经过 JWT、项目权限和 Repository Mutation Lock。 +- 管理面与 RPM 数据面分离部署,Repository Node 对外提供稳定 HTTP 源;OCManager 中可查询同步、上传、Revision 和发布状态,部署关系与复现步骤保留在本文和验收文档中。 +- GPG 完成 RPM 包级签名校验闭环。未签名、错误签名或未授权 key 的 RPM 进入 quarantine,不进入 active Revision;当前不把 `repodata` 签名、`repomd.xml.asc` 或 `repo_gpgcheck=1` 作为验收前置条件。 +- 代码、部署和 Markdown 文档通过 `OpenCloudOS/ocmanager` Pull Request 提交;运行截图、结构化验收报告和演示视频整理后通过邮件提交。任何凭据、私钥、运行数据库或 RPM 大文件均不进入 PR。 + +Build110 至 Build118 完成了同步范围风险提示、软件包删除任务、已清空仓库的安全删除、GPG 公钥删除闭环、隔离项删除规则和对应前端交互更新。该阶段运行环境使用 Build111 Repository Worker、Build118 Footstone 和 Build118 前端静态资源;Build118 完整后端快照在 OpenCloudOS 9 上通过 Repository Service 测试、Repository Worker 编译测试和 `go build ./...`,前端通过 28 项 Repository 测试及 Vite 正式构建。这些阶段性结果随后纳入 Build129 的完整 `make -C manager repo-acceptance`,Build106 因而成为更早的历史证据。 + +### 8.4 隔离 DNF Case A/B/C + +每个用例使用独立的 OpenCloudOS 9.6 最小容器,执行时禁用全部默认源。 + +| Case | 启用仓库 | 安装包 | 来源验证 | 耗时 | 结果 | +| --- | --- | --- | --- | ---: | --- | +| A 仅同步仓 | sync repo | `ocm-sync-only-1.0.0-1.oc9.noarch` | 仅来自生成的 sync repo | 1534 ms | PASS | +| B 仅私有仓 | private repo | `ocm-private-only-1.0.0-1.oc9.noarch` | 仅来自生成的 private repo | 1275 ms | PASS | +| C 私有包依赖同步包 | sync + private | `ocm-private-app` + `ocm-sync-dependency` | app 来自 private,dependency 来自 sync | 1290 ms | PASS | + +Case C 实际安装摘要: + +```text +Installing: ocm-sync-dependency-1.0.0-1.oc9.noarch 1/2 +Installing: ocm-private-app-1.0.0-1.oc9.noarch 2/2 +Installed: ocm-private-app-1.0.0-1.oc9.noarch +Installed: ocm-sync-dependency-1.0.0-1.oc9.noarch +DNF exit code: 0 +``` + +验证命令显式包含: + +```bash +dnf --disablerepo='*' \ + --enablerepo=',' \ + --setopt=install_weak_deps=False \ + -y install ocm-private-app + +rpm -q ocm-private-app +rpm -q ocm-sync-dependency +``` + +### 8.5 OpenCloudOS 官方 RPM 实例 + +#### 8.5.1 四包快速回归 + +快速回归使用 OpenCloudOS 官方源中的小、中、大不同体积包: + +| RPM | NEVRA | 大小 | 节点结果 | +| --- | --- | ---: | --- | +| `opencloudos-release-9.6-1.oc9.noarch.rpm` | `opencloudos-release-0:9.6-1.oc9.noarch` | 272,429 B | SUCCESS | +| `bash-5.2.15-7.oc9.x86_64.rpm` | `bash-0:5.2.15-7.oc9.x86_64` | 1,923,393 B | SUCCESS | +| `llvm-libs-17.0.6-18.oc9.x86_64.rpm` | `llvm-libs-0:17.0.6-18.oc9.x86_64` | 29,903,169 B | SUCCESS | +| `firefox-140.13.0-1.oc9.ap.2.x86_64.rpm` | `firefox-0:140.13.0-1.oc9.ap.2.x86_64` | 122,520,185 B | SUCCESS | + +RPM 合计 `154,619,176` 字节。产品仓库记录: + +```text +Repository ID: d32f8a53-3acb-4e4d-a143-1cee1f53a55b +Revision ID: 0a5d1e17-1894-451f-a6a8-676576428e5c +Source Node: +Target Agent: +Manifest SHA: d823a8524eebeb5335e1724cdb6f185acb4e468c532f9a2ad51f8927b5ec539b +Repomd SHA: 42ca413924ff76033ef62f838c6a46a353d56c612214445a95539d4bcb0db36e +Target exit: 0 +``` + +目标节点逐项核对 4 个 RPM、6 个 metadata 文件和 `repomd.xml`,`current` 为相对链接: + +```text +current -> revisions/0a5d1e17-1894-451f-a6a8-676576428e5c +OCM_DISTRIBUTION_OK +``` + +目标节点随后在禁用默认源的情况下用 DNF 读取该 Revision,查询到上述 4 个包。该用例同时验证了真实 RPM 下载、repodata、immutable Revision、SHA-256、TMS/Agent 下发和目标原子切换。 + +#### 8.5.2 二十包批量与传输验证 + +仅用四个包可以快速覆盖体积边界,但不足以单独说明批量清单处理和连续传输表现。因此在不替换四包用例的前提下,另建 `oc9-official-batch20-20260906` 仓库,从当前 OpenCloudOS 9 BaseOS/AppStream 选择 20 个真实 RPM。OpenCloudOS 的[软件包管理文档](https://docs.opencloudos.org/en/OCS/AdministratorGuide/Software_Management/)说明了 RPM、DNF/YUM 以及 BaseOS/AppStream 源的组织方式;本次样本包含系统基础工具、包管理组件、运行库、内核和大型应用,文件大小从 `17,277 B` 到 `122,485,189 B`。 + +| RPM | 官方源 | 大小 | 分级 | +| --- | --- | ---: | --- | +| `git-2.43.7-3.oc9.x86_64.rpm` | `AppStream` | 17,277 B | `SMALL` | +| `python3-3.11.6-31.oc9.x86_64.rpm` | `BaseOS` | 32,309 B | `SMALL` | +| `createrepo_c-0.20.1-8.oc9.x86_64.rpm` | `AppStream` | 71,033 B | `SMALL` | +| `gzip-1.12-7.oc9.x86_64.rpm` | `BaseOS` | 157,717 B | `SMALL` | +| `jq-1.7.1-9.oc9.x86_64.rpm` | `BaseOS` | 203,141 B | `SMALL` | +| `opencloudos-release-9.6-1.oc9.noarch.rpm` | `BaseOS` | 272,429 B | `SMALL` | +| `curl-8.4.0-17.oc9.x86_64.rpm` | `BaseOS` | 374,809 B | `SMALL` | +| `libarchive-3.7.1-14.oc9.x86_64.rpm` | `BaseOS` | 391,545 B | `SMALL` | +| `xz-5.4.7-8.oc9.x86_64.rpm` | `BaseOS` | 407,329 B | `SMALL` | +| `dnf-4.16.2-5.oc9.noarch.rpm` | `BaseOS` | 480,293 B | `SMALL` | +| `sqlite-libs-3.42.0-10.oc9.x86_64.rpm` | `BaseOS` | 693,865 B | `SMALL` | +| `wget-1.21.3-9.oc9.x86_64.rpm` | `AppStream` | 800,917 B | `SMALL` | +| `vim-minimal-9.0.2092-24.oc9.x86_64.rpm` | `BaseOS` | 814,497 B | `SMALL` | +| `coreutils-9.4-9.oc9.x86_64.rpm` | `BaseOS` | 1,201,649 B | `MEDIUM` | +| `bash-5.2.15-7.oc9.x86_64.rpm` | `BaseOS` | 1,923,393 B | `MEDIUM` | +| `glibc-2.38-49.oc9.2.x86_64.rpm` | `BaseOS` | 2,484,581 B | `MEDIUM` | +| `openssl-libs-3.0.12-27.oc9.x86_64.rpm` | `BaseOS` | 2,599,205 B | `MEDIUM` | +| `llvm-libs-17.0.6-18.oc9.x86_64.rpm` | `BaseOS` | 29,903,169 B | `LARGE` | +| `kernel-core-6.6.119-51.5.oc9.x86_64.rpm` | `BaseOS` | 49,076,681 B | `LARGE` | +| `firefox-140.15.0-1.oc9.ap.2.x86_64.rpm` | `AppStream` | 122,485,189 B | `XLARGE` | + +下图读取当前仍保留的 `acc-20261022-sync` 二十包同步仓库,用于说明最新版软件包清单。它是独立实验样本,不是对本节 2026-09-06 原始仓库和传输数据的重新取证;原始样本、数量、摘要与耗时继续以下表和原始证据为准。 + +![当前独立二十包同步仓的清单示例](images/repository/repository-current-20261024-batch20-packages.png) + +本组 RPM 合计 `214,391,028 B`。官方源下载与签名清点耗时 `91,441 ms`,`createrepo_c` 生成测试上游元数据耗时 `397 ms`。产品链路结果如下: + +下载阶段对 20 个文件逐一执行 `rpmkeys --checksig --verbose`:20 个 Payload SHA-256 均为 `OK`,签名 key ID 均为 `b1ee0065`;每个文件另记录独立 SHA-256,后续同步、HTTP 下载和节点分发均使用该清单交叉核对。 + +```text +Repository ID: 324f9a0a-3254-47af-bf57-fcf577c7dda6 +Revision ID: e0879096-f103-4080-9ebd-2413c5dd1ab6 +Sync Task: 8a0e1660-e0ad-4c3d-ae4a-414a8529129b +Storage Node: +Preflight: PASS (free 33,361,195,008 B; required 1,932,735,284 B) +Sync duration: 6,716 ms +Commit state: CONFIRMED +``` + +产品生成的 `.repo` 被直接用于隔离验证。命令显式禁用全部默认源后,`repoquery` 返回 20 条记录,source repo 均为 `ocmanager-4b3e140e3a88437dbe6bbc8cfc833743`。随后通过该 HTTP 仓库实际下载 `git`、`llvm-libs` 和 `firefox` 三个样本,共 `152,405,635 B`,耗时 `2,463 ms`,三个文件的 SHA-256 均与官方源清单一致。`repomd.xml` 的 `HEAD` 返回 200,写请求返回 403。 + +固定 Revision 随后经现有 TMS/Agent 下发到独立目标 Agent: + +```text +Distribution ID: eddfea6d-4f4b-468e-bbb7-094e0920c5ab +TMS Task: t-70e2dd01-0176-462e-ac66-88a8ec6d00ee +Target Agent: 6136807110285060215 / +Target exit: 0 +Duration: 4,675 ms +Manifest SHA: 4af4b5efb97865e4bd8799aacd85dde90c6299e66cb60f2868d562cf29968afe +Repomd SHA: 9106a7932f91c812651c956b49f4d04a03d7249bf77b6fcf9aa214b1d1ab7100 +current -> revisions/e0879096-f103-4080-9ebd-2413c5dd1ab6 +``` + +当前二十包示例仓没有保留分发记录,因此下图仅用另一个真实两包仓的已完成分发记录说明最新版详情布局,不能作为本节二十包分发成功的证据。二十包结果仍由上面固定的 Distribution ID、校验值及原始目标日志证明。 + +![当前两包仓分发详情的界面示例,非历史二十包实验记录](images/repository/repository-current-20261024-batch20-distribution-detail.png) + +目标节点逐项校验 20 个 RPM 的 SHA-256,并与上游原始清单一一比对,全部一致;再只启用分发后的本地仓库执行 `repoquery`,20 个包的 source repo 全部为 `ocm-batch20-distributed`。这组结果证明的是受控环境下约 204.5 MiB 数据的完整批量链路,不把它夸大为发行版全量镜像压力测试;更大源仍应通过容量预估、拒绝策略和后续专项性能测试评估。 + +完整机器可读证据位于 `.run/repository-poc/evidence/v4-batch20-20260906/`,其中 `summary.json` 汇总指标,`fixture/` 保存官方源与签名清单,`product-sync/` 保存 API、Preflight 和 Package 数据,`http-dnf/` 保存隔离查询与下载摘要,`distribution/` 和 `target/` 保存 TMS 分发与目标校验,根目录 `SHA256SUMS` 固化全部文件。 + +#### 8.5.3 私有上传与分发实例 + +本次控制台操作又使用同一个官方小型 RPM 完成了一条独立的“私有上传 → 发布 → 分发 → DNF 查询”链路: + +```text +Private Repository: 2591a624-8c1d-47ae-a299-172a42289697 +Upload Task: 6f2839bc-a57a-441f-99f6-0aac3e971471 +Revision: 25df4214-47ff-4a7a-8a05-7a44f2430ecf +Manifest SHA-256: ca403799bab37e9fbaef8d51f88417bcc37da7e39dfd9feec88923d419d91b85 +Repomd SHA-256: 58ae96c1b02388260b0c60fab0529e9d967efc07b64f4976bca515b746888f46 +Distribution Node: +Target exit: 0 +``` + +目标 Agent 只启用分发后的本地仓库执行 `repoquery`,结果明确记录软件包来源: + +```text +opencloudos-release-0:9.6-1.oc9.noarch | ocmanager-private | 272429 +``` + +随后执行隔离的 `dnf download`,实际下载文件大小为 `272,429 B`,SHA-256 仍为 `a1b8fd960f4ba09acd85f6fe0cb98dc1e43ebbd7745d4df1e02678a378748134`。通过节点 HTTP 下载得到的摘要也与上传前完全一致;`repomd.xml` 的 `HEAD` 请求返回 200,而 `POST/PUT/PATCH/DELETE` 均返回 403。由此可以同时证明文件没有在上传、发布或分发过程中发生变化,且公共 Repository 数据面保持只读。 + +### 8.6 GPG 结果 + +| 用例 | 预期 | 结果 | +| --- | --- | --- | +| 授权 key 签名 RPM | 校验通过、发布、隔离 DNF 安装成功 | PASS | +| 未签名 RPM | 拒绝并进入 quarantine | PASS | +| 未授权 key 签名 RPM | 拒绝并进入 quarantine | PASS | +| 远程同步签名 RPM | Repository Node 验签成功 | PASS | +| 私钥边界 | DB、Git、public 和 Evidence 中不存在私钥 | PASS | + +2026-09-09 又通过真实产品 API 在 `oc9-private-main` 完成一次可现场演示的 GPG 正向链路。仓库先注册指纹为 `2EBA73A916C0E44421AEC1FD01A017A2531A56E5` 的授权公钥,再上传 `ocm-gpg-demo-1.0-1.noarch.rpm`;任务 `788c15e4-d075-4be2-b9ee-bfe63a97b832` 成功发布 Revision `818f55d3-8a84-4a7f-80c2-b21983202aa5`。产品生成的 `.repo` 使用 `gpgcheck=1`、`repo_gpgcheck=0` 和真实公钥 URL;一次性 OpenCloudOS 9 环境禁用全部默认源后,`dnf` 与 `yum` 均从该 Repository 导入公钥并成功安装,最终 `rpm -q` 返回 `ocm-gpg-demo-1.0-1.noarch`。Repository 数据目录只发现授权公钥,未发现 GnuPG 私钥目录、`secring` 或私钥文件。 + +Build116 将这条人工命令链固化为 `repository-gpg-install-demo.sh`。它与自包含的 `repository-gpg.sh` +互补:前者用于证明当前控制台生成配置和当前发布节点确实可安装,后者用于自动重建授权签名、未签名、 +未授权签名三类样例并验证 quarantine。Build116 的脚本结果必须以实际运行目录中的 `summary.json` +和 `SHA256SUMS` 为准,不能仅凭历史安装结果判定通过。 + +2026-09-10 在独立 OpenCloudOS 9 节点首次执行该脚本,读取页面下载的 +`oc9-demo-private.repo`,在全新 installroot 中完成验证。`repomd.xml` 返回 HTTP 200,公钥 fingerprint 为 +`2EBA73A916C0E44421AEC1FD01A017A2531A56E5`;`repoquery` 返回 +`ocm-gpg-demo|0:1.0-1.noarch|ocmanager-98d47bd3498c4b4fb734475426099080`,DNF 只启用该 Repository, +成功导入公钥并安装 `ocm-gpg-demo-1.0-1.noarch`。DNF 和 RPM 查询退出码均为 0,总耗时 14 秒。 +最终脚本复测的证据保存在当次 `${OCM_REPOSITORY_DEMO_ROOT}/runs/<时间戳>/`,归档目录为 +`.run/repository-poc/evidence/20260910-build116-gpg-install-demo-final/`;`SHA256SUMS` 全部校验通过, +`summary.json` SHA-256 为 `4f55bd0b887ea49463eac60aadcb98eafa03b6639317fc74deaa39d347fcd1b4`。 + +Build117 进一步修复了空仓库删除时的 GPG 公钥闭环。后端不再仅凭 `active_revision_id` 是否存在判断公钥在用,而是查询活动 Revision 的实际软件包数量:有包时继续返回保护性错误,无包时允许删除公钥。前端同步取消了对删除按钮的静态隐藏,并在确认框中说明活动版本仍有包时会被拒绝。OpenCloudOS 9 聚焦回归覆盖授权签名上传、隔离 DNF 安装、未签名和未授权签名隔离、签名远程同步、远程节点验签和私钥边界,结果全部 PASS;Evidence 位于 `.run/repository-poc/evidence/20260910-build117-gpg-delete/`,`summary.json` SHA-256 为 `bf1c0240ba977e0f4f2685c3df9c0806a44d5d5a5b86ef68fe7db027a06ec0b2`。 + +Build118 将 quarantine 明确为不参与发布的拒收内容,不再要求用户为了删除仓库逐条处理隔离项。后端删除检查保留活动软件包、未完成上传、GPG 公钥和运行中任务四类保护,并在同一事务内清理该仓库的隔离元数据后删除仓库定义;前端删除预检不再请求隔离区数量,也不再把隔离项列为阻断原因。OpenCloudOS 9 上的 Repository Service 测试、Worker 编译检查和 `go build ./...` 均通过,前端 28 项测试及正式构建通过。运行态只读核对显示 `oc9-demo-private` 为“活动包 0、隔离项 1、公钥 0、活动任务 0”,部署 Build118 后已满足删除条件;仍有 3 个活动包的 `oc9-private-controlled` 继续被安全保护。Evidence 位于 `.run/repository-poc/evidence/20260910-build118-quarantine-delete/`,目录内 `SHA256SUMS` 已完整校验。 + +### 8.7 大日志结果 + +| 指标 | 结果 | +| --- | ---: | +| 原始日志大小 | 1,073,741,905 B | +| zstd 压缩后大小 | 1,482,164 B | +| 日志行数 | 128,532 | +| 数据库摘要 | 65,536 B | +| 流式处理 | PASS | +| 敏感信息脱敏 | PASS | +| MySQL 只存元数据 | PASS | +| 只读下载和保留清理 | PASS | + +### 8.8 失败诊断与 OCAI + +规则诊断已覆盖: + +- 上游不可达、DNS、HTTP 4xx/5xx、metadata、download、checksum 和架构不匹配。 +- invalid/damaged RPM、错误签名、NEVRA 冲突和危险归档。 +- Repository 不可达、包缺失、依赖缺失/冲突、metadata 和 GPG 失败。 + +Build100 已完成 OCAI 实际 SSE、上下文预览、session 关联、建议反馈、脱敏和服务不可用降级验收。OCAI 只负责分析失败摘要并给出建议,不自动修复或执行任务;用户确认后由 OCManager 创建重试任务。 + +2026-09-08 的 Build105 增量复测确认 Repository 上下文生成、SHA-256、页面预览、analysis/session 创建、OCAI 健康检查和反向代理均正常。VMware NAT 恢复后,真实 `deepseek-v4-flash` 请求返回 HTTP 200,SSE 事件包含 `turn_start`、`thinking`、`text`、`data`、`metrics` 和 `done`,`done_count=1`、`error_count=0`;1,458 B 上下文和 71,481 B 响应的敏感字段扫描命中均为 0。完整响应不落入 Evidence,仅保存模型清单、量化摘要和 SHA-256。增量 Evidence 位于 `.run/repository-poc/evidence/20260908-build105-ocai-sse-after-nat/`,其中 `summary.json` SHA-256 为 `607f5f8a3965a2a11886898692fc8d918d2b5ad34c77ca8e2bc835cc7d6f95f8`。 + +随后将上述过程固化为 `make -C manager repo-ocai`,并接入 `repo-acceptance`,消除了全新环境依赖手工预置 OCAI 摘要的问题。Build106 使用正式脚本再次分析同一损坏 RPM 任务:HTTP 200、`done_count=1`、`error_count=0`,耗时 16,924 ms,响应 72,721 B;上下文与响应敏感字段命中均为 0,七项内部检查全部 PASS。Evidence 位于 `.run/repository-poc/evidence/20260908-build106-ocai-automation/`,`summary.json` SHA-256 为 `badc41f6d79ddcbeab46697d11d1cb3d858add3066fe2437825b60a39ddb6d05`。 + +### 8.9 恢复与安全负向 + +| 项目 | 结果 | +| --- | --- | +| API/worker/Nginx restart | PASS | +| Compose redeploy,保留 DB 和 bind data | PASS | +| disposable 环境 destroy/restore | PASS | +| TMS 空闲边界重启和 Agent 重连 | PASS | +| 恢复后再次执行 acceptance | PASS | +| Repository ID、Revision、manifest、inventory、GPG 和报告一致 | PASS | +| HTTP `GET/HEAD` | PASS | +| HTTP 写方法拒绝 | PASS | +| 空间不足时 active Revision 不变 | PASS | + +2026-09-08 再次执行整机重启恢复检查。管理面 MySQL、Redis、ClickHouse、Footstone、前端和 OCAI 均恢复健康;已部署的仓库节点重新上报 `ONLINE`,目标 Agent 为 `active`。主仓库的相对链接、`repomd.xml` SHA-256、RPM 和 repodata 文件均与重启前一致,staging 中没有遗留任务目录。发布端 `GET/HEAD` 为 200,`POST` 为 403;管理面容器不挂载 Repository 数据目录,控制面与 RPM 数据面分离关系未改变。具体 Revision、摘要和文件计数只保存在结构化 Evidence 中。 + +本次运行的是保留历史镜像和卷的 disposable 验收栈,其中部分容器的 `restart` 策略为 `no`,因此重启后按“基础设施 → Footstone → 前端”顺序启动原容器;正式 Compose 已统一使用 `restart: unless-stopped`。本次恢复摘要保存在 `.run/repository-poc/evidence/20260908-vm-restart-recovery/summary.txt`,SHA-256 为 `77924a26f47a4e89f8a8d128cbd44fc0b4c73a82aa9f84358bc087d3f96aedb0`。第一次检查记录了 VMware NAT 外网 TCP 阻断;重启 NAT 服务后,三台节点均恢复 DNS 和 443,并完成上述 OCAI SSE 复测。整个过程中仓库读取、节点心跳和不可变 Revision 未受影响。 + +当前页面复测期间还修复了两处 MariaDB 运行兼容问题。节点选择查询原先使用 `LEFT JOIN LATERAL`,在当前 MariaDB 中触发 SQL 1064;改为复用已有参数化 IP 映射查询后,节点列表接口恢复 200。分发结果加载原先沿用了绑定在主表上的 GORM session,导致目标表查询出现未知列;现在使用独立 session 明确查询 `repository_distribution_targets`。两处修改均增加了聚焦回归测试,随后通过当前页面完成节点查询、Revision 分发和逐节点结果查看。 + +2026-09-11 的 Build128 补充了任务 lease 恢复时的日志连续性。真实同步任务进入 RUNNING 后停止 Footstone 25 秒,管理面恢复后同一任务以 `attempt=2` 被重新领取并成功发布 Revision `ada8fdf1-9696-4310-ab0f-cd07b84259ca`,不可变地址的 `repomd.xml` 返回 HTTP 200。上一 attempt 的压缩日志和 metadata 被保留在 `task-logs//recovered/`,当前日志 API 继续读取固定路径,整个过程没有再次出现 `TASK_LOG_INIT_FAILED`。两个 Repository Worker 节点均部署并运行 Build128;相关 tasklog、worker 单元测试和 OpenCloudOS 9 构建通过。 + +### 8.10 Evidence + +Build100 最终 Evidence: + +```text +.run/repository-poc/evidence/v4-final-20260904/ +└── 20260904-build100-full-v3/ + ├── release-acceptance/ + │ ├── summary.json + │ ├── ac-results.json + │ ├── category-summary.json + │ ├── dnf/ + │ ├── gpg/ + │ ├── distribution/ + │ ├── diagnosis/ + │ ├── log-governance/ + │ └── reports/ + ├── recovery/ + ├── bundles/ + └── SHA256SUMS +``` + +顶层 SHA-256 清单覆盖 163 个 Evidence 文件,复核结果为 0 个失败;49 个 JSON 文件全部可解析,凭据特征扫描未发现泄露。 + +Build106 重启后全量 Evidence: + +```text +.run/repository-poc/evidence/v4-final-20260908/ +└── 20260908-build106-release-sudo/ + ├── release-acceptance/ + │ ├── summary.json # 31/31 PASS + │ ├── ac-results.json + │ ├── ocai/ + │ ├── dnf/ + │ ├── gpg/ + │ ├── distribution/ + │ ├── diagnosis/ + │ ├── log-governance/ + │ └── reports/ + ├── recovery/ + ├── bundles/ # 9/9 PASS + └── FULL_SHA256SUMS +``` + +远端 Evidence 压缩包 SHA-256 为 `f3558437520e7ecc2659feea73d0525a19612a64fa2a7a0685df7643930f64d9`,回传到本地主仓后校验一致。管理员临时密码、JWT、DeepSeek API Key 和完整模型回答均未进入该目录。 + +Build128 lease 恢复 Evidence: + +```text +.run/repository-poc/evidence/20260911-build128-tasklog-recovery/ +├── control-plane/ # 构建、测试和真实中断恢复结果 +├── node-01/ # 任务日志归档、repomd 和 Worker 运行状态 +├── node-02/ # 第二节点镜像校验与 Worker 运行状态 +└── */SHA256SUMS +``` + +其中 `control-plane/runtime-recovery/summary.json` 记录初次执行、中断、lease 回收、日志重开、标准 repodata 和 HTTP 发布六项检查全部 PASS。Evidence 只保存结构化结果和哈希,不包含节点 sudo 密码、JWT、OCAI API Key 或私钥。 + +### 8.11 Build129 历史候选完整验收 + +2026-09-11 使用本地主仓生成 Build129 完整版本化快照,在 OpenCloudOS 9.6 上重新执行总入口。源码压缩包上传前后 SHA-256 为 `560d8d7b62e061d80b2aefc19fda0286b5d931b38e9305eadbb78b9cf1d1cc60`,关键后端、前端、自动化脚本和中文文档逐文件一致;快照排除了 Git 元数据、运行 Evidence、依赖目录、构建产物和私有环境文件。 + +固定 Worker 基础镜像站在首轮执行时出现 DNS 解析失败。本轮新 Footstone 镜像已经由 Build129 源码构建,Worker 使用 Build128 同一候选源码构建且通过 tasklog/worker 测试的本地镜像;通过 `P12_REUSE_IMAGES=1` 仅跳过外部基础镜像 metadata 下载。随后 r3 在交互 sudo 会话中重新运行全部产品测试,没有复用 r2 中因无 TTY 而未执行的 DNF、GPG 或报告结果。 + +为消除全新环境对失效镜像别名的依赖,Repository Worker 的默认基础镜像随后改为 OpenCloudOS 官方固定标签 `docker.io/opencloudos/opencloudos9-minimal:9.6-v20260714`,并保留 `REPOSITORY_RUNTIME_IMAGE` 覆盖入口。使用已导入的同版镜像执行离线 Docker build POC,容器内 `ID=opencloudos`、`VERSION_ID=9.6`;隔离网复现时按部署文档使用 `docker load` 和 SHA-256 核对。 + +最终结果为: + +```text +Repository Acceptance + +Core Automation PASS (6/6) +Core DNF Validation PASS (4/4) +Core Package Ingestion PASS (7/7) +Core Repository Management PASS (7/7) +Core Repository Publishing PASS (3/3) +Core Security PASS (1/1) +Enhanced Diagnosis PASS (1/1) +Enhanced Distribution PASS (1/1) +Enhanced Repo Generator PASS (1/1) + +31/31 PASS, 0 failed, 0 blocked, 0 pending +Release Acceptance: PASS +``` + +本轮包含三层恢复、TMS 7 服务重启、单目标与多目标 Agent 重连、当前源码后端回归、Repository 前端 29 项测试和正式构建、Case A/B/C、32 类规则诊断、1 GiB 日志、真实 `deepseek-v4-flash` SSE、GPG 正负向与真实产品分发。分发绑定源 Repository、Revision 和 manifest SHA-256,从仓库节点向独立目标 Agent 下发后,目标端 manifest、repomd 与原子切换全部通过;历史验收使用两个目标作为多目标增强证据。 + +最终 Evidence 已回传到 `.run/repository-poc/evidence/v4-final-20260911/build129-release-candidate-r3/`。159 个文件的内部清单和 JSON 解析均为 0 失败,凭据特征扫描为 0 命中;压缩包 SHA-256 为 `f2386ac6d5e069a57018cc52ba3245cfd49d2cc8604dc643cafb0e15d3b7f6fe`。 + +### 8.12 Build130 官方 OC9 镜像完整验收 + +Build130 将 Repository Worker 的默认运行镜像固定为 OpenCloudOS 官方 `docker.io/opencloudos/opencloudos9-minimal:9.6-v20260714`,并保留内网镜像覆盖能力。完整源码包上传前后 SHA-256 为 `e7b4141d7fb357a6026063809e19e56c1ec4da9b0f4de93053d90b4a9de8d47d`;关键配置、Compose 与恢复脚本逐文件哈希一致,Compose 在提供必填节点参数后解析通过。 + +本轮不使用恢复镜像复用开关,Footstone 和 Repository Worker 都从该 Build130 源码构建。新 Worker 镜像以官方固定标签作为基础,容器内确认 OpenCloudOS 9.6。随后完整执行 31 项验收,结果为: + +```text +31/31 PASS, 0 failed, 0 blocked, 0 pending +9/9 acceptance categories PASS +Release Acceptance: PASS +final_ready: true +``` + +最终 Evidence 位于 `.run/repository-poc/evidence/v4-final-20260911/build130-official-oc9-image-r1/`。162 个文件的内部哈希清单、JSON 解析和凭据特征扫描均为 0 失败,压缩包 SHA-256 为 `5327f2841a89cbfa9474a881b3d41fa62dba686af5a249270ef1337ce6412f5f`。在当时的版本序列中,Build130 取代 Build129 成为新的完整验收依据。 + +### 8.13 提交前可复现性复核 + +提交整理阶段继续在 OpenCloudOS 9.6 上检查源码快照,没有使用 Windows 或 WSL 结果代替目标环境证据。Repository 后端专项包、TMS API 全量测试、前端 29 项 Vitest、全量 Stylelint 和 Vite release build 均通过;基础设施、管理面、TMS 和 Repository Node 四套 Compose 在填写必需节点变量后均可解析。 + +复核发现原一键部署脚本只显式执行到第 8 个 Footstone 增量迁移,新环境可能缺少诊断、分发、GPG、仓库节点、任务提交和上游来源表。当前 `deploy.sh up` 已按自然版本顺序执行第 3 至第 14 个迁移,第 12、13 个结构变更增加重复执行保护,第 13 个迁移补充明确的 `USE footstone`。在 OpenCloudOS 9.6 上使用 disposable MariaDB 11.4 连续导入两轮,5 个关键表、6 个任务提交字段和 2 个任务索引均完整存在,两轮均为 PASS。 + +提交前检查扩展为扫描候选源码中的全部 Shell 文件,并固定 `.sh`、`.sql` 使用 LF。12 个部署 Shell 回归全部通过;源码扫描未发现实验网段、个人目录、真实 API Key、私钥、运行 `.env` 或临时 Evidence。前端安装仍报告与上游依赖树同级的历史审计项,未使用会引入破坏性升级的 `npm audit fix --force`;当前结果无 critical,Repository 测试与正式构建不受影响。 + +### 8.14 Build141 历史增量候选复核 + +Build141 针对实际操作中发现的两类残留问题做了小范围收敛。第一类是 include 中混入不存在包名时,DNF 默认严格行为会让整次同步失败;现在任务使用非严格下载配置,已匹配包照常进入完整新 Revision,未匹配项合并为一条 `WARN`,全部未匹配时才以 `SYNC_EMPTY_RESULT` 失败。第二类是上传在 GPG 预检、无包变更或取消阶段提前结束时,Upload 可能没有同步进入终态;现在本地与远程 worker 路径都以幂等方式把它收敛到 `FAILED`,并让已经超过 `expires_at` 的历史记录退出仓库删除保护。 + +前端在原有通用上游源模型上增加 `mirrors.opencloudos.tech` 的 BaseOS/AppStream 两项内置来源,与官方源共四项。页面仍保留“添加项目上游源”和“自定义地址”,快捷包组按来源、通道、版本和架构显示,不把 OpenCloudOS 特例写入后端协议或同步任务状态机。 + +| 复核项 | 结果 | +| --- | --- | +| 四个后端专项包 `go test` / `go vet` | PASS | +| Repository Vitest | 30/30 PASS | +| 全量 Stylelint、Vite 正式构建 | PASS | +| 修改文件聚焦 ESLint | PASS | +| `bash` + 不存在包混合同步 | PASS:仅发布 `bash`,任务含未匹配 WARN | +| 全不存在包同步 | PASS:`SYNC_EMPTY_RESULT`,未发布 Revision | +| GPG 前置失败上传 | PASS:Task 与 Upload 同为失败终态,空仓库可删除 | +| Build141 页面实拍 | PASS:仓库列表、四个来源、来源联动包组均来自当时运行页面 | + +Build141 的 Footstone 和两个 Repository Worker 已在 OpenCloudOS 9 环境健康运行,前端静态资源可从管理入口访问。本节记录的是当时的增量聚焦 Evidence,因此不改写 Build130 的 31/31 全量结果;后续正式候选的上游合并和完整 `repo-acceptance` 已由 Build143 完成,见 8.16 节。 + +### 8.15 Build142 历史上游合并候选复核 + +2026-09-12 从当时最新的上游 `master`(`853dba6`)创建隔离 worktree,将功能基线与 Build141 增量作为普通 Git 合并内容接入,没有复制整棵目录覆盖上游。25 个共同修改文件中出现 11 个文本冲突,处理时保留上游 JWT/HMAC、CA 加密与单一签发方、部署镜像和公共路由更新,同时保留 Repository 服务、节点 mTLS 端口、TMS 分发和自动化入口。冲突标记与未合并文件均为 0。 + +候选源码由 Git tree 生成归档并在 OpenCloudOS 9.6 上校验 SHA-256。发布检查扫描 1448 个文件,Shell 语法、JSON、Markdown 图片、凭据特征和可移植性全部通过。前端 Repository Vitest 为 30/30 PASS,全量 Stylelint、Vite release build 和修改文件聚焦 ESLint 通过;后端 Repository Worker、Repository Service、Content Service、Application 及冲突相关实例查询测试通过,Repository 相关包 `go vet` 和 `go build ./...` 通过。 + +上游 permission 包的全量 `go vet` 仍有既有 Mutex copy 告警,纯源码目录的部分实例仓库测试依赖 Footstone 运行配置和数据库,不把这些历史环境项写成全仓通过。本节只说明 Build141 增量可以与当时上游合并;该候选上的完整 Release Acceptance 已在 Build143 执行,见下一节。 + +### 8.16 Build143 上游集成 Release Acceptance + +Build143 对当时最新的上游合并候选执行了完整而非复用式的产品验收。候选 Git tree 为 `67da943f37ff5b6a41859e763ed97231996ff7ad`,源码归档 SHA-256 为 `52941ee05e6c1c210b8965bf78c6f722f31f0b77d763e0cc56c9bfe277eb7362`。归档上传 OpenCloudOS 9.6 后重新校验哈希,发布检查扫描 1448 个文件并通过。 + +首轮执行暴露了两项环境可复现问题。恢复辅助容器原先固定从 Docker Hub 获取 Alpine,在隔离网络下无法启动;现在通过 `P12_HELPER_IMAGE` 显式指定已验证镜像。四个 DNF 测试 `.spec` 在 Windows 工作区曾被检出为 CRLF,`rpmbuild` 会把回车识别为命令;现在 `.gitattributes` 固定 `*.spec text eol=lf`,候选改由 Git tree 生成,并先单独验证 Case A/B/C 为 3/3 PASS。两项修复都只影响自动化和打包边界,没有修改 Repository schema、RPC 或任务状态机。 + +最终总入口在交互 sudo 会话中运行,并使用进程级 Go Module 代理补齐锁定依赖。恢复脚本重新验证 restart、redeploy、destroy/restore、任务 lease、mTLS、只读发布和状态摘要一致;TMS 七个服务重启后,两个已注册 Agent 均重新连接并完成任务。后续后端回归、前端 30 项测试与正式构建、报告导出、Case A/B/C、32 类规则诊断、1 GiB 日志、真实 OCAI SSE、GPG 正负向和产品分发均在同一次执行中 PASS。 + +产品分发绑定真实 Repository、immutable Revision 和源 Storage Node,并下发到独立目标 Agent。目标返回的 manifest SHA-256 和 repomd SHA-256 与控制面记录一致,原子发布 PASS;具体运行 ID 只保存在结构化 Evidence 中,不写成部署固定值。最终 AC-01 至 AC-31 为 `31/31 PASS`,9 个外部分类为 `9/9 PASS`,恢复和 Evidence Bundle 均为 PASS。 + +完整 Evidence 已回传到 `.run/repository-poc/evidence/v4-final-20260912/build143-upstream-integration-r1/`,归档 SHA-256 为 `f3e598e58b087456163a26799c122d0c87db6edae05d9b57107cf9981f9c1c4c`。Evidence 不包含管理员密码、JWT、OCAI API Key 或私钥。Build141 之后到 Build143 没有页面交互变更,因此本报告中的 Build141 截图是该历史完整验收对应的页面实拍。 + +### 8.17 Build152 干净 OC9 从零部署补充验证 + +为避免只在已有服务和数据上验证,项目又在独立新建的 OpenCloudOS 9.6 x86_64 虚拟机上执行了一次从空数据卷开始的完整部署。源码归档上传前后 SHA-256 均为 `218b0e49c4f4fb49f69452284d50ce24d4ddedaf7e188514fc8c0ea6b36e3e50`,对应源码树 `dbed251f44e4a861f6e793bbf047de3ec6d12b75`;发布检查扫描 1454 个文件,未发现 CRLF Shell/Go 文件、私有凭据、绝对开发路径或缺失文档图片。 + +这次复现实际发现并收敛了两个部署边界。第一,MySQL 容器的 `healthy` 不能代表首次初始化时最终 root 凭据已经可用,部署脚本必须等待真实鉴权查询成功后再执行迁移。第二,Docker 镜像、Alpine APK 和前端 npm 依赖是三条独立网络链路,均需要可配置地址和独立预检;npm 镜像切换只改变安装来源,没有改动锁文件或依赖版本。 + +部署最终通过 42 项环境预检、MySQL 8.0.35 两轮迁移、全栈镜像构建和 17 项运行健康检查。后端聚焦测试、前端 30 项 Vitest、Vite 正式构建、仓库三个隐藏路由的真实登录访问、Case A/B/C、GPG 正负向、32 类规则诊断、1 GiB 日志治理均通过。首页和主机页也完成实测,主机列表相关三个接口均返回 HTTP 200,没有复现旧 MariaDB SQL 1064。随后在 disposable 环境完成 restart、应用 redeploy 和 destroy/restore,恢复前后 Repository ID、active Revision、manifest、包清单、GPG 元数据和 Validation 状态摘要一致。 + +部署日志没有运行时 `ERROR/FATAL/PANIC`。剩余告警是 disposable 环境的公开默认密码和锁定前端依赖树中的 deprecated/audit 提示;生产环境必须替换默认密码,本轮不使用 `npm audit fix --force` 引入超出仓库功能范围的破坏性升级。 + +Build152 的 Core Acceptance 为 `27/31 PASS`、`0 FAIL`、`3 BLOCKED`、`1 IN_PROGRESS`。该干净虚拟机只部署基础控制面,没有部署 OCAI、Repository Storage Node 或任何 Agent,分别影响 AC-24、AC-25、AC-29 和聚合 AC-31;但基础预检、16 个容器、17 项健康检查、登录和主机查询均正常。该结果证明可选组件缺失不会阻断基本组件部署。完整功能的分发验收只要求至少一个在线目标 Agent;当环境提供多个 Agent 时,同一接口可执行多目标分发,Build143 的两个 Agent 结果属于更强的历史验证证据。 + +### 8.18 Build154 当前主线干净基础部署验证 + +Build154 使用与提交 `1fa9b1e3e61b55cd4459e27904e2da37da302d05` 相同的 Git tree `a23037dd6098ee4e89c0ad18b62b5f7f297f380a` 生成候选归档,归档 SHA-256 为 `6458e8831771345a5ebf9e765fa13a715da837f3d8c96fc3a4c5ee024c30fdfe`。源码在 OpenCloudOS 9.6 解包后重新检查,Shell 和 Go 文件均为 Linux LF,发布检查扫描 1456 个 Git 文件并通过。 + +本轮保持 `ENABLE_OCAI_DEPLOY=false` 和 `REPOSITORY_NODE_INTERNAL_ENABLED=false`,不部署 OCAI、Repository Worker、Repository Publisher 或 Agent。环境预检 `42/42 PASS`,基础设施、TMS 和 manager 共 16 个容器启动,健康检查 `17/17 PASS`,前端返回 HTTP 200。结果证明基础控制面不依赖这些附加组件。 + +同一候选树在 OpenCloudOS 上完成 `go test`、`go vet`、`go build ./...`、MySQL 8.0.35 迁移兼容测试、Repository Shell 测试、前端 Repository/路由 `28/28` 测试和 Vite 正式构建。Build154 是当前主线最近一次干净基础部署证据;由于该拓扑没有启用 OCAI、Repository Node 和 Agent,它不替代历史 Build143 的完整功能 `31/31 PASS` Evidence。 + +### 8.19 提交前干净拓扑聚焦修复 + +在独立 OpenCloudOS 9 控制节点、Repository Node 和 Agent 上按文档从空环境部署时,又发现七个可复现的集成边界。本轮只做对应位置的最小修复,没有修改数据库 schema、Repository RPC、任务状态机或前端页面结构: + +| 问题 | 最小修复 | 聚焦证据 | +| --- | --- | --- | +| Footstone 直连严格 TMS 时只有身份头 | TMS client 对 JSON 与 multipart 请求体按既有 canonical 规范签名;Compose 从当前 shell 注入共享密钥 | tmsclient 单元测试覆盖发布与制品上传 | +| 不同 Agent 包模板的默认 Gateway 文本不同 | 新增配置规范化脚本,按 YAML 列表位置更新首个地址并校验结果 | `configure_agent_package_test.sh` | +| systemd Agent 未设置 `HOME` | 分发脚本按显式 mirror root、`HOME`、passwd home 的顺序解析;均无值时明确失败 | distribution 聚焦测试 | +| Repository Node 首次部署漏编译 Worker | 部署文档在 Compose 启动前增加 `build-bin-static` 和二进制检查 | 文档命令与 Compose 构建路径复核 | +| 筛选同步与全量同步共用 64 GiB 预估 | 按 Include 自动分级:筛选为 2 GiB/1000 包,全量为 64 GiB/100000 包;管理员分别覆盖且全量值保持硬上限 | 配置、预检和 Worker 模式选择单元测试 | +| 同一页面切换 `repository_id` 不刷新 | 监听路由 ID,重置分页、详情状态并重新加载 | Repository Vitest | +| 新建 OCAI 会话先查询不存在的 session | 新分析携带一次性 start 标记,直接发起首个 SSE;历史会话仍按原路径加载 | Repository/OCAI Vitest | + +本轮没有要求部署人输入个人 home 目录。无人值守服务使用交互提示会使自动部署挂起,因此改为可选运行变量和标准账号解析;源码、脚本及提交文档均不包含实验 IP、账号目录、密码或 API Key。历史章节中的地址、容量和多节点数量只表示当时 Evidence,不作为产品固定前置条件。 + +## 9. 最终效果 + +| 原问题 | 实现结果 | +| --- | --- | +| RPM 分散在主机目录 | 统一 Repository 模型、节点和 Package 清单 | +| 手工执行 createrepo | worker 自动生成和验证 repodata | +| 上游包与私有包边界不清 | BASELINE/UPDATE/PRIVATE、产品版本、Revision 和来源字段 | +| 每次建仓都手填 URL 和过滤规则 | OpenCloudOS 内置来源、项目地址模板、快捷规则与自定义输入并存 | +| 大仓库占用管理服务目录 | RPM、repodata 和原始日志迁至独立的 Storage Node 服务;生产环境可单独部署 | +| 同步前不知道磁盘是否足够 | 容量、inode、工具和预计峰值 Preflight | +| 上传损坏包可能污染仓库 | fail-closed 校验、quarantine 和 active Revision 不变 | +| 发布过程可能被读到半成品 | immutable Revision + 原子相对链接 | +| 安装成功但来源不可信 | 禁用默认源并记录 provider/source repo 的 Case A/B/C | +| 大日志进入数据库 | JSONL + zstd,MySQL 只存摘要和索引 | +| 失败原因难定位 | 稳定错误码、规则诊断和只读 OCAI 辅助分析 | +| 仓库无法下发到业务节点 | immutable Revision 绑定的 TMS/Agent 分发 | +| 业务侧手工编写 `.repo` | 真实策略驱动的 Generate/Copy/Download | +| 验收步骤依赖人工 | `make -C manager repo-acceptance` 执行核心流程并严格校验已准备的 152 项结果,缺失项非零退出 | + +当前产出已经形成从“配置、同步/上传、校验、发布、安装、诊断、分发、恢复到 Evidence”的完整产品链路。Repository 目录符合标准 YUM/DNF 格式,可被 OpenCloudOS 9 的 `dnf/yum` 直接使用;所有增强项均作为最终 MUST 纳入 Release Acceptance。 + +以下为历史稳定性实验,不代替当前回归:Build119/Build122 修正真实大源筛选时机后,OC9.6 BaseOS 的 `curl` 筛选任务在 16.486 秒内仅接收 1 个 RPM,生成可读取的标准 repodata,没有先下载完整 BaseOS。该实验支持筛选行为的设计;当前功能仍以 Task 实际配置和结果为准。 + +Build124-Build127 继续完成架构矩阵与排队稳定性。AArch64 的 BaseOS/AppStream 四组快捷包和 Noarch 的三组快捷包均通过真实 API、远程同步、包清单、架构、Revision、repodata 与 HTTP 发布验证。最终矩阵为 `7/7 PASS`,共发布 25 个真实 RPM、`38,288,545` 字节;其中 2 个任务在排队超过 5 分钟后使用原容量估算和节点最新状态生成了新预检记录,更新 `preflight_id` 后正常发布。Noarch 候选包从 x86_64/aarch64 元数据入口下载后的 SHA-256 一致,发布链路仍强制只接受 noarch RPM。 + +消费侧继续在干净 OpenCloudOS 9.6 最小容器中禁用全部默认源,只使用产品生成的 7 个动态 Repo Config 执行 `dnf repoquery`,25 个目标包的架构和 `repoid` 全部匹配。该结果证明这些 Revision 能被 DNF 读取且包确实来自 OCManager;AArch64 在当前 x86 主机上只验证元数据解析和来源,不代替原生 AArch64 安装。同步发布 Evidence 位于 `.run/repository-poc/evidence/20260910-build125-architecture-matrix/`,消费侧 Evidence 位于 `.run/repository-poc/evidence/20260910-build127-architecture-consumer/`,两处 `SHA256SUMS` 均已在远端和本地完整校验。 + +Build128 又验证了长任务在管理面短时中断后的收敛能力:任务使用同一 ID 重新领取,旧 attempt 日志被保留,新 attempt 成功完成并发布,不会因日志文件已存在而阻断恢复。该增量 Evidence 位于 `.run/repository-poc/evidence/20260911-build128-tasklog-recovery/`。 + +Build130、Build141、Build143、Build154 分别保留早期完整基线、聚焦修复、核心验收和干净基础部署证据;它们不是当前版本号,也不把当时两个 Agent 的拓扑写成产品固定要求。当前版本的能力范围、152 项结果与双架构测试见第 11 章。 + +## 10. 相关文档 + +- [私有化软件仓库方案设计](私有化软件仓库方案设计.md) +- [私有化软件仓库部署说明](私有化软件仓库部署说明.md) +- [私有化软件仓库测试验收](私有化软件仓库测试验收.md) +- [V3.1/V4.0 架构和阶段记录](architecture/private-repository-plan.md) +- [Repository 管理 Proto](../manager/backend/protocol/repository/repository.proto) +- [Repository Node Proto](../manager/backend/protocol/repositorynode/repository_node.proto) +- [前端 Repository API](../manager/frontend/src/service/repository.ts) +- [Repository Node Compose](../manager/deployments/compose/repository-node.compose.yml) +- [验收矩阵](../scripts/repository-acceptance-matrix.json) + +## 11. 当前版本结构化验收闭环 + +V3 将V2 产品化能力纳入同一套机器验收,不以旧版 `31/31` 展开冒充新结果。权威预期集合由 `manager/acceptance/repository-acceptance-criteria.json` 定义,实际结果逐项保存实现、测试和 Evidence 指向。 + +公开归档的[最终候选验收摘要](evidence/repository-acceptance-20261014.json)包含 152 个 ID 的实现/测试指向、原始证据摘要校验值、核心矩阵、恢复结果与总入口退出码。它是 2026-10-14 已完成验证的记录,不是可直接复用的本次结果文件;敏感运行环境和原始日志留在本地归档。重新部署应按测试文档生成自己的 Evidence,不把归档结果当作新环境已通过。 + +### 11.1 重点闭环项 + +本表保留已完成实验的验证描述;图片文件使用当前界面重新展示相应入口或既有结果,不充当 2026-10-14 的原始附件。历史执行证据见上面的最终候选归档,2026-10-24 当前候选完整验收见 11.10。 + +| ID | 实现 | 验证 | Evidence | +| --- | --- | --- | --- | +| 64 | Package Install 使用一个 Task 管理多个 Package 与 Target,目标状态独立保存 | 两个目标均完成 Preview 和安装,失败目标排序与结构化字段由同一接口提供 | `repository-current-20261024-package-install-preview.png`、`repository-current-20261024-package-install-result.png` | +| 100 | 批量删除进入一个后端 DELETE_PACKAGE Task,逐包结果写入 Task Result Item | 两个包在同一 Task 内成功,只生成一个新 Revision 并重建一次 repodata | `repository-current-20261024-batch-delete-preview.png`、`repository-current-20261024-batch-delete-result.png` | +| 126 | Inventory Import Job 关联 Repository Task,Task 是统一状态来源 | Preview 后创建 INVENTORY_IMPORT Task,可在全局任务页查询 | `repository-current-20261024-inventory-import-preview.png`、`repository-current-20261024-inventory-import-task.png` | +| 142A | Repository、Artifact、Install Source、Enrollment 和 Agent 使用统一架构规范化 | x86_64 与 aarch64 节点分别只展示匹配的仓库内容 | `repository-current-20261024-node-drawer.png`、`repository-current-20261024-arm-node-drawer.png` | +| NODE-06/07/08 | Node Drawer 提供存储概览、Repository 列表、同步入口和最近任务 | 从 Node 页面创建原有 Sync Task,任务成功后 Revision、包数和同步状态同步刷新 | `repository-current-20261024-node-drawer.png` | + +![节点视角 Repository 同步闭环](images/repository/repository-current-20261024-node-drawer.png) + +![批量删除逐包结果](images/repository/repository-current-20261024-batch-delete-result.png) + +![双架构仓库节点隔离](images/repository/repository-current-20261024-arm-node-drawer.png) + +### 11.2 最终判定规则 + +P13 完成功能、E2E、安全、恢复、双架构和候选源码冻结;P14 仅收口四份权威文档、截图、Evidence 链接和 PR 材料。最终门禁要求: + +```text +repo-acceptance exit code = 0 +expected/actual IDs = 152/152 +duplicate/missing/unexpected IDs = 0 +non-PASS IDs = 0 +failed_count = 0 +P13 后核心业务源码 SHA-256 未变化 +Documentation verification PASS +``` + +只有上述条件全部满足后,结构化报告才可以写入 `152/152 PASS`、`OVERALL_ACCEPTANCE=PASS` 和 `FINAL_READY=YES`。历史 Build 结果继续作为对应阶段证据保留,不覆盖最终候选结果。 + +### 11.3 最终结果 + +2026-10-14 的最终候选已实际满足上述条件。OpenCloudOS 9 总入口完成完整恢复、TMS 双目标重启恢复、后端和前端回归、Case A/B/C、GPG、真实 OCAI、1 GiB 日志治理和不可变 Revision 分发;核心矩阵为 `31/31 PASS`,9 类 Evidence Bundle 为 `9/9 PASS`。 + +152 项结果校验为:预期 152、实际 152、重复 0、缺失 0、意外 0、非 PASS 0、失败 0、Evidence 不完整 0。P14 前后 1019 个核心业务文件的 SHA-256 清单完全一致,说明最终文档收口没有改变已经验证的业务代码。 + +```text +152/152 PASS +OVERALL_ACCEPTANCE = PASS +FINAL_READY = YES +``` + +### 11.4 残余说明与本次文档复核 + +最终候选验证期间,TMS 聚合健康探针曾因 File Service 空 MD5 探测返回 `degraded`;当时真实 TMS 重启任务和 Repository 分发均通过,因此按未阻断本功能的上游探针问题记录,没有顺手改动 TMS。该记录不保证任意时刻所有 Agent 均在线,现场操作仍需先核对当前心跳。 + +2026-10-15 文档复核重新采集创建/来源筛选、节点运维、Node RPM 安装源/白名单、上传、Repo 配置、任务与 OCAI 等当前页面,并验证文档链接和发布材料。该轮没有重跑完整 152 项业务流程,也没有修改 Repository 业务源码;只补齐迁移测试对现有 v15~v18 的断言。 + +### 11.5 独立修复候选专项验证 + +2026-10-23 将老师认可的 `05312e2` 固定保留,新增修复仅在 `codex/repository-review-fixes-20261023` 工作树中实施,未提交、推送或更新原 PR。下面是本次定向验证,不覆盖 11.3 的历史结果,也不为当前候选重写 `FINAL_READY=YES`。 + +| 修复 | 实现与验证结论 | +| --- | --- | +| FIX-01 | 临时 DNF 元数据信任准备后才 assumeno;真实 OC9 目标缺包的 Preview 正向通过,错误 Metadata Key 被拒,Preview 不改变目标 RPM/公钥库;生成的 Confirm 脚本可真实安装 | +| FIX-02 | compact JWS 原结论由合法请求反证,未放宽 JWT;新增相同直接父任务的在途 Retry 去重,真实 MySQL 8 并发仅一个子任务 | +| FIX-03 | 独立租约维护、Context 上限与 Worker 启动恢复;同一个生产 Client 经真实 TCP/mTLS 断连及恢复后继续完成任务,OC9 重复 10 次通过;真实 DB 另验 lease/fencing/互斥 | +| FIX-04 | 唯一错误目录补齐 GPG 与三类上传对象错误,保留 Code、中文原因/建议和原始错误;目录单测通过 | +| FIX-05 | 对象缺失/不可读/不匹配区分,合法 FAILED Upload Retry 仍受任务、冻结对象与租约校验;真实 DB 验证成功结果原子导入及重复回报,普通 FAILED 成功结果仍拒绝 | +| FIX-06 | 陈旧状态刷新先于列表筛选/全量 Summary;真实 DB 验证 31 秒离线、DISABLED 保留、容量保留和合法心跳恢复 | +| FIX-07 | 保留已完成的 sysdiagnose HMAC、Compose 注入与运行健康检查;签名单测及三场景健康契约通过。STRICT 上线和真实命令结果沿用本次接手前执行记录,不伪称再次下发 | +| FIX-08 | 参数合法 0/false、自定义参数、跨页目标和错误反馈修正;补修 TDesign 切页挂载竞态。浏览器回填及命令预览通过,未自动执行 | +| FIX-09 | 光盘分类统一用于 KPI、曲线和告警;真实页面排除光驱 100% 假告警,原始 API 数据仍保留,真正数据盘满载/只有光驱另有组件测试 | + +第 7.6 节的全新安装也已补验:产品生成的 immutable `.repo` 保留 `gpgcheck=1` 与 `repo_gpgcheck=1`,只启用内部仓库,实际安装 bash 及依赖共 19 个包,DNF 与 `rpm --root -q` 退出码均为 0。演示脚本同时补齐多公钥下载与首次元数据信任准备。 + +![独立修复候选:原参数与目标回填,未下发执行](images/repository/repository-current-20261024-command-helper-rerun.png) + +![独立修复候选:命令预览,不是执行结果](images/repository/repository-current-20261024-command-helper-preview.png) + +![独立修复候选:磁盘趋势明确排除光盘](images/repository/repository-current-20261024-disk-chart.png) + +截图中的私网地址仅标识实验样本,不是产品默认或部署前提。公开[专项摘要](evidence/repository-review-fixes-20261023.json)保存实现、测试引用、原始证据摘要哈希与实际范围;完整原始日志/源码快照继续保留本地。复现命令见[测试文档第 12 节](私有化软件仓库测试验收.md#12-独立审查修复候选的定向复核)。 + +本节是当时的源码专项记录,未替换运行镜像;后续实际替换与真实 RPM 发布另见 11.6,不追溯改写本节证据。未重跑全部 152 项;额外 transport race 被上游 tnet checkptr 中止,未关闭检查。既有大 chunk 告警和相邻旧 SFC 历史 lint 问题仍保留。 + +### 11.6 界面统一与候选产物上线回归 + +2026-10-23 将 Repository 查询区对齐 OCManager 既有 SearchBox 风格,修复节点/任务 Drawer 标签拉伸和固定列表头错位。未改全局主题、API、数据库或任务链路。已有修复候选的 Footstone、Worker 与本轮正式前端静态产物已替换到现有 OC9 测试环境,保留镜像/二进制备份、原 Compose 配置、证书与一致的非空 HMAC;STRICT 仍为 true。未新建 VM,也未推进 Gitee/原 PR。 + +![仓库列表:统一查询和固定列对齐](images/repository/repository-current-20261024-list.png) + +![任务详情:手动与成功标签按内容宽度展示](images/repository/repository-current-20261024-task-success.png) + +浏览器覆盖 1440×900、1920×1080 和 768px 宽度:四个一级列表及软件包查询区对齐,页面无整体横向溢出;仓库/任务表头与数据列误差为 0px;在线/手动/成功标签样本宽度 42px,保留各自状态色。输入 Enter 与清空均只触发一次查询;SHA 短摘要单行显示。表格原分页、列设置、固定操作列和吸附横向滚动条保留。 + +| 验证层 | 本次实际结果 | +| --- | --- | +| 前端质量 | Repository 17 文件 109/109;相邻运维定向 42/42;局部 ESLint、Stylelint、Repository 类型检查、正式构建 PASS | +| 后端定向 | Content、Task、Node、Package Install、Error Catalog、Worker 与 sysdiagnose/TMS 包测试 PASS;不等于所有外部集成用例重跑 | +| 上传/重试 | 新建 ENFORCE 仓,7 个分片乱序/暂停恢复/幂等重传;缺公钥失败后登记正确公钥,重试原 Task 发布成功、Session COMPLETED;同名文件使用独立 Session | +| GPG/安装 | 新包和 Metadata 双签名;TMS 首次 Preview READY,Confirm 安装两包成功;产品 immutable 配置在新 installroot 真实安装并查询成功 | +| 同步/分发 | 受控源同步成功;两个现有在线 Agent 分发均 SUCCESS/exit 0 | +| 导入/删除 | 显式 Key 映射后导入成功且 Task 可查;依赖 Preview 隔离系统源;两包一个 Task/一个新 Revision/两条结果;错误 Digest 和 Revision 变化拒绝 | +| 统计/日志/OCAI | 全量 Node Summary 与独立 Task 统计正确;后端日志 tar.gz 下载成功;仓库 Drawer 真实 SSE 分析到“分析完成”,不自动执行建议 | + +同内容再次上传返回 `NO_PACKAGE_CHANGES` 是当前状态机的明确拒绝,不把它写成新增包成功;原始错误码和技术信息保留。本次测试只影响专属新建资源,历史仓库未改。测试镜像缺 `jq/awk` 时脚本正确中止,补齐隔离测试工具后通过,未关闭预检或签名校验。 + +公开[本次产物与联验摘要](evidence/repository-style-validation-20261023.json)保存产物 SHA、任务/Revision 引用和原始证据摘要;操作前提和复现步骤见[测试文档第 13 节](私有化软件仓库测试验收.md#13-界面统一与已部署候选的联验)。截图中地址仅为实验数据,不是部署默认值。 + +当前完成到“界面统一 → 候选替换 → 主要业务联验 → 文档更新”。**尚未在本次候选上完整重跑 152 项**:aarch64 节点离线、Node RPM/白名单竞态和恢复/破坏性全矩阵未重跑;因此本次不更新 `FINAL_READY=YES`。完整 PR/发布验收仍按既有候选冻结与受控全量门禁执行,旧 152/152 仅属于原验收日期。 + +### 11.7 运行态故障与恢复实验 + +2026-10-24 在现有两台 OC9 测试 VM 上补做三组真实运行实验,无新增 VM、无数据库手工修复、无 commit/push/PR 更新。实验前队列空闲,仅操作新建专属仓;先正常同步建立可用版本,再对专属上游的元数据请求注入 60 秒延迟,避免破坏已有源。 + +发现并修复一个取消路径缺陷:底层 DNF 进程被 Context 取消后可能只返回 `signal: killed`,日志记为 FAILED,但 Worker 按取消信号上报 CANCELED,管理面一致性校验拒绝,任务等待租约回收。修复仅在日志执行器根据实际已取消 Context 保留取消语义,不放松后台校验。两项新分支测试、Worker 包测试及 vet 通过,新 Worker 已按 SHA 核验备份后替换。 + +| 实验 | 结果 | +| --- | --- | +| 修复后在途取消 | 约 10.2 秒转 CANCELED_BY_USER;活动发布未被取消任务改变;后续新同步成功 | +| 控制面中断约 25 秒 | Worker PID 不变,自动重连;同 Task attempt 1→2、fencing 6→7,新 Revision 发布成功 | +| 同步中重启 Worker | 同 Task 经租约恢复 attempt 1→2、fencing 8→9,新 Revision 发布成功 | +| 收尾 | 服务正常、节点 ONLINE、无在途任务;临时慢源已关闭,旧产物和所有任务证据保留 | + +[本次机器可读恢复摘要](evidence/repository-recovery-validation-20261024.json)保存原始证据 SHA 和 Task/Revision;[测试文档第 14 节](私有化软件仓库测试验收.md#14-运行态故障与恢复实验)定义可复现步骤和边界。本次 Worker SHA 已不同于 11.6 的历史联验产物,11.6 结果保留原日期和范围,不冒充新 Worker 全量验收。仍未完成本次候选完整 152 项、ARM、白名单竞态和 destroy/restore 门禁,`FINAL_READY` 继续为 NO。 + +### 11.8 当前候选补验 + +本节保留完整门禁前的早期补验时点,最终结果见 11.10。 + +2026-10-24 后续实际补齐真 MySQL 白名单授权竞态、授权后精确恢复、v17 带数据升级 v18、隔离恢复、TMS 全服务重启、DNF Case A/B/C、实际限速并发、Mirror/Append 删包差异、AUDIT 发布以及双目标软件安装。结果和可复用入口见[测试文档第 15 节](私有化软件仓库测试验收.md#15-当前候选补验记录)。11.6、11.7 的未覆盖描述保留为历史边界,不代表这些补验仍未执行。 + +双架构当前源码 Node RPM 已原生构建、真实签名并通过标准 Repository 上传/校验/repodata/immutable 发布;x86_64 和 aarch64 均通过 TMS 从该签名源安装并以原 Node ID 自动注册,版本心跳已更新。ARM 安装期间 firewalld 重载使实验临时 NAT 通路失效,恢复仅属于实验的规则后原任务自然成功;没有重发 Token、改数据库或降低 TLS/GPG。 + +新前端多文件队列已完成真实 HTTP 并发、按字节进度、暂停、刷新重选补缺片和跨仓隔离验证,两个 Upload Task 均成功发布。节点 Drawer 同步、多仓容量和任务过滤也已补验。Node RPM 安装候选进一步收敛到当前项目/架构的启用源,解绑源的 Artifact 仅作历史记录。旧版本号测试包的发布和页面选择已补验,不冒称执行过运行节点降级。 + +[当前补验摘要](evidence/repository-final-validation-20261024.json)提供实际测试及原始证据哈希,不将验收校验器测试中的人工 fixture 当作功能结果。 + +![当前上传队列入口;本节多文件完成结果以原始 Session/Task 证据为准](images/repository/repository-current-20261024-upload-queue.png) + +![当前节点安装入口与 RPM 版本参数;本次截图未安装或降级](images/repository/repository-current-20261024-node-install.png) + +![当前修复候选:统一任务查询区和内容宽度状态标签](images/repository/repository-current-20261024-tasks.png) + +本节两张入口图和任务列表使用 2026-10-24 当前页面重拍;多文件完成、历史版本选择的原始截图仍在对应补验证据中保留,不以空队列图替代完成证据。图中地址、名称和测试版本不是部署默认。下面进度保留原补验时点,不表示截图刷新后重新回到该状态:该补验时点的机器进度为 **144 PASS / 3 NOT_STARTED / 5 BLOCKED**,共 152 个唯一 ID;不是“152/152 PASS”。剩余包含聚合回归、统一发布门禁、最终文档/证据收口及凭据轮换。前端最新为 Repository 20 文件、126 项通过;后端真实配置 529 项通过,Presentation 30 项及 Application integration 14 项通过,opt-in 补验与未重跑的原始分发单测明确区分,详见测试文档第 15 节。 + +[152 项机器结果清单](evidence/repository-acceptance-progress-20261024.json)逐项保留实现、测试及公开证据指向;原始本地结果 SHA 也一并保存。早期严格结果验证实际返回退出码 1:预期/实际/唯一 ID 均为 152,没有重复、缺失或意外 ID,仍有 8 项非 PASS。`failed_count=0` 仅表示没有标为 FAIL 的条目,不表示尚未完成或阻塞项可以通过。 + +外部 OCAI 密钥安全项后续已完成:用户确认旧密钥撤销,新值仅写入仓库外受保护运行配置,供应方鉴权 HTTP 200、OCAI 健康检查和仓库任务真实流式分析均通过。该[外部轮换阶段](evidence/repository-credential-rotation-20261024.json)只重建 OCAI,镜像及其他服务未变。之后另行完成内部 HMAC/JWT 与 MySQL/Redis/ClickHouse 凭据轮换,见[内部认证摘要](evidence/repository-internal-authentication-rotation-20261024.json)及 11.9;外部阶段保留的内部 `PENDING` 仅代表原时点,不是最新总体状态。安全专项通过仍不替代完整发布门禁;该阶段仍保持当时的 144/152 与 `FINAL_READY=NO`;最终结果另见 11.10。 + +以上是完整门禁前的独立候选补验记录;当时尚未完成全部机器结果及文档/产物一致性,未把专项 PASS 改写为最终 PASS。后续完整门禁和 P14 收口见 11.10,仍未提交、推送或更新认可 PR。 + +### 11.9 运行认证和错误目录后续复查 + +内部轮换实际验证了旧 HMAC 401、新签名 200、严格模式保持;共享 JWT 运行配置一致,新鲜产品登录成功。旧 Token 的 API 请求失败,但未观察到明确的 signature error Code,因此报告不归因到某个未经证实的底层错误。MySQL root/应用、Redis、ClickHouse 的旧凭据拒绝,新凭据实际鉴权成功,ClickHouse Distributed 查询正常;Redis 临时持久化标记和 Repository/Task/Revision 指纹保持,原镜像、挂载和数据卷不变。私有回滚材料留在仓库外受限目录,未作为公开 Evidence;ZooKeeper 没有启用认证,本次未将未使用的密码变量冒充为 SASL 能力。范围和哈希见[内部认证轮换摘要](evidence/repository-internal-authentication-rotation-20261024.json)。 + +轮换后真实登录、Node 查询和 OCAI 分析复查见[产品复查摘要](evidence/repository-post-rotation-product-validation-20261024.json):两架构 Node 保持 ONLINE,当前版本相同;真实失败任务的 OCAI 上下文可构建、分析完成,分析前后 Repository Task 数均为 128;已经成功的 `INVENTORY_IMPORT` 可以经统一任务详情查询。本次没有通过 OCAI 执行修复或新建 Repository 变更任务。原始完成后 Dialog 文字检测值保留,不用该检测代替实际任务计数和 API 证据。 + +后端已有 Node RPM 保护拒绝现已统一接入唯一错误目录。Catalog 6 项、Presentation HTTP 映射 10 项、受影响包 vet 和 manager build 通过;新 Footstone 二进制经 SHA 核对替换并健康。实际 API 中,安装源同步、确认删除和架构不匹配版本选择均被拒绝,同时返回稳定英文 Code、中文原因、中文建议和原始技术原因;拒绝前后 Task 数未变。见[7 文件与运行验证摘要](evidence/repository-node-rpm-error-validation-20261024.json)。未给删除确认的参数错误仍返回英文 `InternalError` 提示,不能把本次专项写成所有错误分支已经中文化。 + +可复用的独立验收数据库操作前提和命令已补入[测试验收 15.1](私有化软件仓库测试验收.md#151-独立容器-mysql-模式):使用显式 loopback 高端口、独占标签容器、受保护密码文件和 server UUID 核验;未选择该模式时保留原 socket/sudo 行为。它不连接部署 MySQL,也不改变产品部署模型。这一后续复查时点的完整 Makefile 门禁仍在执行,当时为 `OVERALL_ACCEPTANCE=NOT_PASS`、`FINAL_READY=NO`。该历史状态不代表最终候选;11.10 记录实际完整门禁与 P14 结果。 + +### 11.10 当前候选最终完整验收 + +2026-10-24 在 OpenCloudOS 9 对冻结候选实际执行 `make -C manager repo-acceptance`,退出码为 0;核心/增强矩阵 31/31、Evidence Bundle 9/9、全部 11 个执行步骤及严格 152 项校验均通过。预期、唯一预期、实际、唯一实际均为 152,重复、缺失、意外、非 PASS 和不完整证据条目均为 0。结果见[最终候选摘要](evidence/repository-final-validation-20261024.json)、[152 项机器结果](evidence/repository-acceptance-progress-20261024.json)和[完整发布验收](evidence/repository-release-validation-20261024.json)。 + +```text +152/152 PASS +OVERALL_ACCEPTANCE = PASS +FINAL_READY = YES +``` + +全量运行发现的 GPG 临时密钥用例使用了过长 Unix socket 路径,已只在测试夹具采用受控短临时目录并完整重跑;隔离运行器同步固定工具路径和最小环境注入。此收口没有修改 Repository 业务链路或放松 GPG/mTLS/Mutation Lock。源码清单、锁文件和运行产物指纹与证据绑定,P14 只更新文档与证据链接。 + +152 项结果逐项保存 implementation、tests 和 evidence,预期集合仅由 `manager/acceptance/repository-acceptance-criteria.json` 定义,不从 Markdown 推导。完整发布运行之后仅进行文档、证据链接和截图材料收口;再次核验非文档源码 SHA-256 未变、文档链接和验收引用存在,原 PDF 未覆盖。 + +本次通过是独立修复候选的本地验收结果,不意味着已修改 Gitee/原 PR。任何后续业务源码变更均应使受影响证据失效并重新验证。 + +验收范围继续保持明确:ZooKeeper 的历史部署未启用认证,本轮未扩展 SASL;Repository 定向类型检查通过不等于全站历史 TypeScript 问题已清零;旧版本号 RPM 已验证页面选择与 EVR 策略,不声称对运行节点执行过历史代码降级;TMS Agent 需先按既有 TMS 部署方式安装并在线,本轮自动安装的是 Repository Node RPM,不是为新机器自动引导部署 TMS。遗漏删除确认的普通参数错误仍可能返回英文技术提示,不能据此宣称所有参数校验均已中文化。 diff --git "a/docs/\347\247\201\346\234\211\345\214\226\350\275\257\344\273\266\344\273\223\345\272\223\351\203\250\347\275\262\350\257\264\346\230\216.md" "b/docs/\347\247\201\346\234\211\345\214\226\350\275\257\344\273\266\344\273\223\345\272\223\351\203\250\347\275\262\350\257\264\346\230\216.md" new file mode 100644 index 0000000000000000000000000000000000000000..628177cb00645ae3fc2bb687f90dfc03a0b22af2 --- /dev/null +++ "b/docs/\347\247\201\346\234\211\345\214\226\350\275\257\344\273\266\344\273\223\345\272\223\351\203\250\347\275\262\350\257\264\346\230\216.md" @@ -0,0 +1,930 @@ +# OCManager 私有化软件仓库部署说明 + +| 项目 | 内容 | +| --- | --- | +| 文档版本 | V1.0 | +| 更新日期 | 2026-10-25 | +| 适用读者 | 系统管理员、OCManager 运维人员和项目部署人员 | +| 部署基线 | OpenCloudOS 9;使用仓库提供的 Linux shell、Compose 和部署脚本 | +| 功能基线 | `ddbdfea457644ed7d01e6c209e9dfd2168cd6953` | +| 配套文档 | [方案设计](私有化软件仓库方案设计.md) · [测试验收](私有化软件仓库测试验收.md) · [用户手册](私有化软件仓库用户手册.md) · [结构化验收报告](私有化软件仓库结构化验收报告.md) | + +## 文档说明 + +### 建设背景 + +OCManager 私有化软件仓库为隔离网、专有云和企业内部环境提供 RPM 仓库管理。它支持上游同步、私有包上传与校验、repodata 和不可变 Revision 发布、TMS 节点分发,以及 YUM/DNF 客户端配置生成。稳定运行需要管理面、仓库存储节点、节点执行能力和客户端访问按职责完成部署;本说明按 OCManager 现有组件关系,给出从环境检查到仓库客户端验证的操作流程。 + +### 适用范围与部署前提 + +本文覆盖管理面、Repository Storage Node、TMS/Agent、OCAI、只读仓库发布和恢复检查。基础管理面至少需要一台 OpenCloudOS 9 主机;创建、同步、上传和发布仓库需要至少一个在线 Repository Storage Node;TMS 安装或 Revision 分发需要匹配架构且在线的 Agent。OCAI 用于任务分析,可按部署档位启用。各节点角色可独立部署,也可在资源和隔离条件允许时共用宿主机。 + +命令以 OpenCloudOS 9 Bash 环境和仓库脚本为准。部署路径、网络地址、镜像源和 Secret 由部署人员按目标环境配置。安全凭据通过环境变量或受限权限文件注入,不写入公开文档、命令历史或仓库配置。 + +2026-10-24 的候选在 OpenCloudOS 9 完成 152/152 机器验收、31 项核心与增强矩阵及 9 个 Evidence Bundle。该记录描述对应代码基线;新环境仍需执行本文的部署后检查和配套测试。 + +### 部署顺序 + +首次部署依次完成环境和端口检查、管理面启动、首个仓库节点注册、仓库节点能力验证、HTTP/DNF 发布验证以及恢复检查。升级或故障恢复时,先核对当前版本、数据目录和配置备份,再执行相应步骤。日常控制台操作见[用户手册](私有化软件仓库用户手册.md),自动化复现和通过标准见[测试验收](私有化软件仓库测试验收.md)。 + +## 目录 + +**基础部署** + +- [1. 部署目标](#1-部署目标) +- [2. 目录与端口](#2-目录与端口) +- [3. 环境检查](#3-环境检查) +- [4. 部署管理面](#4-部署管理面) +- [5. 准备首个节点与从前端安装后续节点](#5-准备首个节点与从前端安装后续节点) +- [6. 部署 Repository Node](#6-部署-repository-node) + +**发布验证与运行维护** + +- [7. HTTP 发布验证](#7-http-发布验证) + - [7.1 在独立 OC9 节点复现 GPG 安装](#71-在独立-oc9-节点复现-gpg-安装) + - [7.2 Repository Metadata 签名密钥](#72-repository-metadata-签名密钥) +- [8. OCAI 部署边界](#8-ocai-部署边界) +- [9. 重启、重建与恢复](#9-重启重建与恢复) +- [10. 部署完成判定](#10-部署完成判定) + +**发布前和部署后检查** + +- [11. 发布与运行检查](#11-发布与运行检查) + - [11.1 Node RPM 安装源](#111-node-rpm-安装源) + - [11.2 注册授权与证书](#112-注册授权与证书) + - [11.3 双架构与可选拓扑](#113-双架构与可选拓扑) + - [11.4 部署后门禁](#114-部署后门禁) + - [11.5 独立修复候选的上线前检查](#115-独立修复候选的上线前检查) + - [11.6 界面候选的产物替换与回滚](#116-界面候选的产物替换与回滚) + - [11.7 当前候选的补验前提](#117-当前候选的补验前提) + - [11.8 认证轮换与验收数据库隔离](#118-认证轮换与验收数据库隔离) + - [11.9 冻结候选的最终部署验收](#119-冻结候选的最终部署验收) + +## 1. 部署目标 + +软件仓库采用管理面和数据面逻辑分离。OCManager 管理服务保存仓库模型、任务、Revision 清单和日志索引;Repository Node 保存 RPM、repodata、quarantine 和压缩后的原始日志。生产环境推荐将 Repository Node 部署在独立机器;资源受限或演示环境也可以部署在管理节点所在宿主机,但 manager 容器本身不挂载 Repository 数据目录。 + +```text +浏览器 + -> OCManager Nginx + -> Footstone Repository API -> MySQL + -> TMS -> Agent + -> OCAI(只读分析) + +Footstone --HTTPS + mTLS:19443--> Repository Worker +业务主机 --GET/HEAD:18090------> Repository Publisher +``` + +基础部署只需要一台管理节点。启用软件仓库同步、上传和发布时,至少注册一个 Repository Storage Node 服务;启用 Revision 分发时,再准备至少一个在线 TMS Agent。Repository Node 和 Agent 可以与其他角色共用宿主机,也可以分别部署;Agent 数量可以是 0、1 或多个,单目标与多目标使用同一套接口和任务模型。 + +### 1.1 部署档位与故障隔离 + +| 档位 | 必需组件 | 缺少附加组件时的行为 | +| --- | --- | --- | +| 基础控制面 | infra、TMS、manager | OCAI、Repository Node 和 Agent 均可不部署;基础页面、API 和健康检查正常 | +| 仓库管理 | 基础控制面 + 至少一个 Repository Node | 没有 Agent 仍可建仓、同步、上传、发布和生成 `.repo`,只是不执行节点分发 | +| 单目标分发 | 仓库管理 + 一个在线 Agent | 可把 immutable Revision 分发到该目标并校验摘要 | +| 多目标分发 | 仓库管理 + 多个在线 Agent | 用户可选择一个或多个目标,系统逐节点记录成功、失败或部分成功 | +| 智能建议 | 任一上述档位 + OCAI | OCAI 不可用时规则诊断继续工作,AI 建议明确显示不可用,不影响其他任务 | + +Repository Node 可通过 RPM + systemd 安装,也可使用独立 Compose;新节点的前端安装入口见第 5 节。Agent 使用既有 TMS 注册链路,OCAI 仅在 `ENABLE_OCAI_DEPLOY=true` 时启动。可选组件离线只影响对应能力:Worker 离线时拒绝新的仓库 mutation;已经发布的 HTTP 内容只有在 Publisher 和存储仍可用时才继续读取;Agent 离线时不能被选择为分发或安装目标;OCAI 离线时不影响规则诊断。TMS 本身属于 OCManager 基础控制面,不作为可选组件移除。 + +## 2. 目录与端口 + +| 节点 | 路径或端口 | 用途 | +| --- | --- | --- | +| 管理节点 | `config/.env` | OCManager、TMS、OCAI 和 Repository 控制面配置 | +| 管理节点 | `/etc/ocmanager/repository-node-pki` | Footstone 服务端证书、私钥和客户端 CA | +| 仓库节点 | `/opt/ocmanager/repository-data` | RPM、repodata、Revision、quarantine 和日志 bind mount | +| 仓库节点 | `/etc/ocmanager/repository-node-pki` | Worker 客户端证书和节点数据接口证书 | +| 管理节点 | `19443/tcp` | Repository Worker 到 Footstone 的 mTLS 控制通道 | +| 管理节点 | `19444/tcp` | 首次注册与精确重试的 TLS Bootstrap 通道;仅启用自动安装时开放 | +| 仓库节点 | `19443/tcp` | Footstone 到 Repository Node 的 mTLS 数据接口 | +| 仓库节点 | `18090/tcp` | YUM/DNF 公共只读地址,只允许 `GET/HEAD` | + +Repository 数据目录固定为: + +```text +repository-data/ +├── staging/ +├── quarantine/ +├── revisions-private/ +├── task-logs/ +├── keys/ +├── trash/ +└── public// + ├── revisions// + └── current -> revisions/ +``` + +以上为产品默认运行目录,不是开发机路径;部署人员可通过本节后续变量选择宿主机目录。Node/TLS 与 Metadata Signing 私钥不得放入 Repository 数据目录、Git、MySQL、Inventory 或公共 HTTP 路径。Metadata Signing 私钥按第 7.2 节保存在 Node 的独立受限目录。 + +## 3. 环境检查 + +下表是部署和测试的版本边界。构建时不要临时换用更旧的 Node、Go 或非 OC9 RPM 工具链;这类混用会导致前端间接依赖 engine 报错、RPM 签名判定不一致或 `createrepo_c` 行为偏差。 + +| 组件 | 要求 | 历史 Build130 记录 | +| --- | --- | --- | +| 系统 | OpenCloudOS 9 | OpenCloudOS 9.6 | +| Docker / Compose | Docker 20.10+ / Compose v2 | Docker 28.0.1 / Compose v2 | +| Go | 1.25+ | 1.26.3 | +| Node.js | 20+ | Node 20 容器 | +| MySQL | 8.0;仓库 Compose 默认固定 8.0.35 | MySQL 8.0.35 干净初始化 | +| OpenSSL | OC9 官方 `openssl` 命令 | 部署 secret 生成、TMS CA 与 mTLS 证书初始化 | +| RPM / GPG | OC9 官方 `rpm`、`rpm-sign`、`gnupg2` | rpm 4.18 系列 / GnuPG 2.4 | + +管理节点从源码构建时需要 Docker、Docker Compose、Go 以及项目既有构建工具。前端依赖按 `package-lock.json` 使用 `npm ci`,类型检查使用仓库锁定的 `vue-tsc`,不要临时全局安装另一版本。预构建镜像部署无需在所有运行节点安装前端工具链。RPM/systemd Repository Node 需要下列 OC9 工具,RPM 的 `Requires` 会安装运行依赖;Docker/Compose 仅为 Compose 模式所需: + +```text +dnf-plugins-core +createrepo_c +rpm +rpmkeys +rpm-sign +gnupg2 +zstd +Docker +Docker Compose +openssl +``` + +RPM/systemd 部署还使用 RPM 声明的 `nginx`、`gettext`、`policycoreutils-python-utils` 和 `shadow-utils`;测试构建 RPM 另需 `rpm-build`。Metadata Signing 使用 Node 本地 `gpg`,启用前单独安装并验证 `gnupg2`。不要把 Publisher 的 nginx 与管理控制台 nginx 的配置文件混用。 + +管理面运行只需要 Compose 中的 MySQL 8.0.35,不要求宿主机启动 MariaDB。只有执行完整 Repository Acceptance 时,部分隔离测试才需要宿主机提供 `mysql` 兼容客户端和临时数据库服务。在 OpenCloudOS 9 上直接安装 `mariadb-server` 即可同时获得 MariaDB 服务和 `mysql` 兼容命令: + +```bash +sudo dnf install -y \ + mariadb-server dnf-plugins-core createrepo_c rpm-build rpm-sign \ + gnupg2 zstd jq nodejs npm +``` + +不要先安装 OC9 的独立 `mysql` 客户端再安装 `mariadb-server`,二者可能因客户端文件归属发生冲突。已经安装 `mysql` 的 disposable 验收机可用 `dnf install --allowerasing mariadb-server` 完成替换;生产管理节点不应仅为了测试改变现有数据库客户端。 + +部署前选择宿主机数据目录,并检查磁盘、inode、权限与文件系统。下面在当前源码根目录执行,目录可由部署人员覆盖: + +```bash +export REPOSITORY_DATA_HOST_DIR="${REPOSITORY_DATA_HOST_DIR:-/opt/ocmanager/repository-data}" +sudo install -d -m 0750 "$REPOSITORY_DATA_HOST_DIR" +df -h "$REPOSITORY_DATA_HOST_DIR" +df -i "$REPOSITORY_DATA_HOST_DIR" +stat -f -c '%T' "$REPOSITORY_DATA_HOST_DIR" +``` + +在源码根目录运行发布前自检,确认不包含凭据、私钥、个人路径或临时证据,并同时检查 Shell、JSON 和 Markdown 图片链接: + +```bash +make -C manager repo-release-check +``` + +默认镜像从 Docker Hub 拉取。隔离网应先导入相同标签的镜像,然后在私有 `config/.env` 中覆盖 `OCM_IMAGE_MIRROR`、`GO_IMAGE`、`RUNTIME_IMAGE`、`NODE_IMAGE`、`NGINX_IMAGE` 和 `REPOSITORY_RUNTIME_IMAGE`。TMS 的 CGO 服务还会从 Alpine 包源安装工具链,应通过 `APK_MIRROR` 指向可达的公网或内网镜像;前端构建通过 `NPM_REGISTRY` 指定 npm 官方源、可信内网代理或镜像。环境检查会分别探测 Registry、Alpine 包源和 npm `/-/ping`。不要把任何镜像服务密码写入脚本或 Git。 + +容量预检采用: + +```text +required_free = +active_revision_size ++ max_import_expanded_size ++ projected_new_revision_size ++ metadata_reserve ++ 20% safety_margin +``` + +文件系统使用率达到 90%、inode 不足、工具缺失或节点离线时拒绝新的 mutation,但不影响当前 Revision 的读取。 + +## 4. 部署管理面 + +从配置模板创建本机私有配置,不把真实密码和 API Key 提交到 Git: + +```bash +cp config/env.example config/.env +``` + +至少确认以下配置: + +```dotenv +SERVER_HOST=<管理节点地址> +REPOSITORY_NODE_PKI_HOST_DIR=/etc/ocmanager/repository-node-pki +REPOSITORY_PUBLIC_BASE_URL=http://<仓库节点>:18090/repositories +ENABLE_OCAI_DEPLOY=false +OCAI_LLM_DEFAULT_HOST= +OCAI_LLM_DEFAULT_NAME= +OCAI_LLM_DEFAULT_API_KEY= +``` + +| 配置 | 填写规则 | +| --- | --- | +| `SERVER_HOST` | Agent、仓库节点和浏览器都能访问的管理节点 IP 或 DNS,不要在多机部署中填 `127.0.0.1` | +| `REPOSITORY_NODE_PKI_HOST_DIR` | 宿主机私有 PKI 目录;可替换默认 `/etc/ocmanager/repository-node-pki`,但必须同步修改 Compose bind mount | +| `REPOSITORY_PUBLIC_BASE_URL` | 业务主机能访问的稳定发布根地址,形如 `http://:18090/repositories` | +| `OCAI_LLM_DEFAULT_HOST` | DeepSeek 或其他 OpenAI-compatible API endpoint,不包模型名和 API Key | +| `OCAI_LLM_DEFAULT_NAME` | 接口实际公布的模型名;验收使用的 flash 模型也必须先在 OCAI 中配置 | +| `OCAI_LLM_DEFAULT_API_KEY` | 只写入不入 Git 的 `config/.env` 或运行时 Secret,不能写入文档、脚本、镜像或 Evidence | + +基础控制面可以保持 Repository 内部通道与 OCAI 关闭。需要实际运行远端 Node 时,在私有 `config/.env` 中明确启用 `REPOSITORY_NODE_INTERNAL_ENABLED=true`、`REPOSITORY_NODE_DATA_CLIENT_ENABLED=true`,填写 `REPOSITORY_NODE_INTERNAL_PUBLIC_TARGET`、`REPOSITORY_NODE_INTERNAL_SERVER_NAME` 与数据面客户端证书。使用前端 TMS 安装还需 `REPOSITORY_NODE_BOOTSTRAP_ENABLED=true`、可达的 `REPOSITORY_NODE_BOOTSTRAP_PUBLIC_TARGET` 和对应 SAN。启用 OCAI 时将 `ENABLE_OCAI_DEPLOY` 改为 `true` 并填写模型配置。 + +证书目录应由部署账号管理,私钥权限为 `0600`。Footstone 需要 `server.crt`、`server.key`、`ca.crt`;访问 Node 数据接口使用单独的 `footstone-client.crt/key`;证书 SAN 必须覆盖实际访问地址。自动注册使用的签发材料由 `REPOSITORY_NODE_SIGNING_PKI_HOST_DIR` 指定,属于部署人员提供的受保护 CA,不会由普通仓库上传代替。不要把测试 PKI 当作生产信任根。 + +执行现有部署入口: + +```bash +bash scripts/check-environment.sh +YES=1 bash scripts/deploy.sh up +bash scripts/deploy.sh ps +``` + +`deploy.sh up` 在基础设施启动后,会先使用配置中的 root 凭据执行真实查询,并确认 `@@port > 0`,也就是已经从临时 server 切换到最终服务后,才按版本号顺序执行 `v1.0.0_3` 至当前最新的 Footstone 增量迁移,再启动 TMS 和管理服务。MySQL 首次初始化的临时 server 使用 `@@port=0`;此时 `mysqladmin ping` 可能返回存活,最终 root 密码也可能已经可用,但 initdb 脚本仍未执行完成,因此这两项都不能单独作为迁移门禁。脚本默认等待 300 秒,可通过 `MYSQL_AUTH_READY_TIMEOUT` 和 `MYSQL_AUTH_READY_INTERVAL` 调整,但不建议通过固定 `sleep` 或跳过迁移规避初始化竞态。 + +当前同一入口按自然版本顺序发现并执行 `v1.0.0_3` 至 `v1.0.0_18`。首次初始化仍由 MySQL initdb 执行第 1、2 个基础文件;后续升级不手工覆盖已应用 SQL。Repository 增量分工如下: + +| 迁移 | 当前职责 | +| --- | --- | +| v9–v14 | Repository、任务、Revision、上传、独立节点、预检、日志、上游来源及原有修复 | +| v15 `repository_productization` | 平铺统计、Node Usage、同步 Schedule/Run、任务幂等、Preview 与语义 URL 字段 | +| v16 `repository_transfer_and_tms` | Target、Enrollment/证书、Node RPM Artifact、Indexed Chunk 与 Package Install | +| v17 `repository_gpg_import` | AUDIT/ENFORCE、Metadata Signing Key Inventory、Revision 签名和 Inventory Import | +| v18 `repository_acceptance_closure` | 系统 Node RPM 安装源、注册白名单、Bootstrap 授权恢复、逐包结果项、Import 统一 Task 关联 | + +restart/redeploy 通过框架及 SQL 的已存在检查处理重入。v18 已冻结,不应为现场问题修改已应用迁移。迁移失败或鉴权等待超时会立即终止部署,不能跳过错误继续启动。脚本通过容器环境变量向 MySQL 客户端传递口令,不把口令写入命令参数、日志或仓库文件。 + +可在不连接数据库的情况下先检查迁移发现顺序、密码参数边界和失败传播: + +```bash +bash scripts/tests/mysql_metadata_upgrade_test.sh +``` + +正式部署后再核对最新结构是否存在: + +```bash +docker compose -f infra/docker-compose.yml --env-file config/.env \ + exec -T mysql mysql -uroot -p -e \ + "USE footstone; SHOW TABLES LIKE 'repository_task_targets'; SHOW TABLES LIKE 'repository_node_install_sources'; SHOW TABLES LIKE 'repository_node_registration_allowlists'; SHOW TABLES LIKE 'repository_task_result_items'; SHOW COLUMNS FROM repository_inventory_import_jobs LIKE 'repository_task_id';" +``` + +命令中的 `-p` 会交互读取数据库密码;自动化环境应通过受保护的 Secret 注入,不要把明文密码写进命令历史或文档。 + +健康检查: + +```bash +curl -fsS http://127.0.0.1:/health +bash scripts/deploy.sh tms ps +bash scripts/deploy.sh ocai ps +``` + +### 4.1 只升级本次前端交互增量 + +本节仅适用于已正常运行当前 V3 后端、TMS 和所需 Repository Node 的环境。初次安装或旧版后端升级仍完整执行第 4 节及现有迁移流程,不能只换前端跳过后台前置条件。列显示、滚动、节点选择、中文文案和登录反馈的本次变化无需重新安装 Node RPM、重启 Agent 或重建数据库。 + +从 Repository Root 执行以下 Linux 命令。前提是 `config/.env` 已存在,并沿用初次部署时受保护的 Shell Secret 注入环境;Compose 的必需 Token/HMAC 变量未注入时会直接拒绝,不能通过删掉校验或把 Secret 写进文档绕过。构建前记录现有 frontend 镜像 ID/唯一标签,用于回滚: + +```bash +docker compose --env-file config/.env \ + -f manager/deployments/compose/docker-compose.yml images frontend + +docker compose --env-file config/.env \ + -f manager/deployments/compose/docker-compose.yml build frontend + +docker compose --env-file config/.env \ + -f manager/deployments/compose/docker-compose.yml up -d --no-deps frontend + +docker compose --env-file config/.env \ + -f manager/deployments/compose/docker-compose.yml ps frontend +``` + +`restart frontend` 只重启原容器,不会把刚修改的 Vue 源码编进镜像。正式镜像由项目 Dockerfile 构建并自带静态资源;不手工覆盖运行容器中的文件,不复制另一台机器的 `node_modules`,也不为本次升级更换依赖版本。单独执行 `npm run build` 只得到构建产物,不代表现有 frontend 容器已经更新。 + +浏览器访问实际管理面地址与 `FRONTEND_PORT`(Compose 默认 13070),不是固定的开发端口。若通过 SSH 转发,在本机私有连接配置中替换主机与端口,不把实验 IP 写入部署脚本。源码调试前通过私有环境设置 `VITE_FOOTSTONE_TARGET`、`VITE_SYSDIAGNOSE_TARGET`、`VITE_OCAI_TARGET`,使代理能访问对应服务;本机值不入 Git,开发启动不能代替正式 Compose 部署。需要源码调试时另开终端,从 Repository Root 执行: + +```bash +cd manager/frontend +npm ci +read -r -p '开发监听端口(本机空闲端口): ' dev_port +test -n "${dev_port}" +npm run dev:linux -- --host 127.0.0.1 --port "${dev_port}" --strictPort +``` + +升级后验证: + +1. HTTP 页面可达,使用部署账号正常登录;登录处理中按钮有反馈。不要连续输错真实管理员密码来测试锁定,锁定时按真实后台提示处理,不修改数据库绕过权限。 +2. 仓库/任务工具栏可见“显示列”,选择、应用、取消和恢复默认有效;长表的表头及底部横条保持可达。 +3. 软件包安装可搜索/筛选在线执行节点;预览仍绑定 immutable Revision,只检查预览无需实际安装。 +4. 分发列表直接显示历史目标 IP;重复注册导致禁选时先核对 TMS 的 Agent 身份和心跳,不重建仓库或删除未知注册记录。 +5. 需要 OCAI 时从仓库内失败任务详情打开上下文,确认后结果定位到当前 Drawer;未部署 AI 时保留规则诊断,不能假定模型已连接。 + +旧页面残留先检查所访问端口是否指向新镜像,再强制刷新一次或用独立浏览器会话对照。列偏好用“显示列 → 恢复默认 → 应用”重置,不清理整站存储或抹掉登录数据来掩盖问题。若出现 403,区分登录失败与业务权限,不把所有错误都归因于浏览器缓存。前端升级异常时按之前保存的镜像版本恢复 frontend,不触碰 RPM、数据库或节点数据。 + +操作示例和最新页面见[结构化报告第 5 章](私有化软件仓库结构化验收报告.md#5-前端设计),增量检查见[测试文档 1.4](私有化软件仓库测试验收.md#14-控制台交互增量复现)。 + +## 5. 准备首个节点与从前端安装后续节点 + +本节截图取自 2026-10-24 当前控制台,捕获范围和 SHA-256 见[2026-10-24 当前页面截图清单](evidence/repository-documentation-screenshots-20261024.json)。图中的节点、地址、版本与容量仅是实验样本;部署时填写自己的实际参数。图片展示操作入口,不表示新环境可以跳过下述首节点初始化、TMS 接入或授权步骤。 + +![仓库节点列表:先确认节点在线、容量与当前版本](images/repository/repository-current-20261024-nodes.png) + +首个仓库节点承担初始化 Node RPM 私有源的职责,不要求从外部源同步发行版。没有可用 Node RPM 源时,先由部署人员使用第 6 节的 Compose + 手工身份方式启动首个 Repository Node;该首节点不是由尚不存在的安装源自举。随后即可在控制台完成以下产品流程: + +1. 在“仓库管理”创建允许上传、无外部上游的私有仓库,绑定首个节点;按所需策略注册 RPM 公钥。 +2. 在相同架构的 OC9 构建机生成 `ocmanager-repository-node` RPM,上传到该私有仓,等待任务成功和新 Revision 发布。 +3. 打开“仓库节点 → 安装仓库节点 → Node RPM 安装源”,选择架构、该私有仓和 `ENABLED`,保存安装源。仓库将标记为 `system_managed=true / purpose=NODE_RPM`,关键配置与删除受到保护。 +4. 在“发布 Artifact”中从该源已发布的包选择 Node RPM,填写其真实 Build Commit 并登记。Artifact 绑定项目、包、NEVRA、SHA-256 和 immutable Revision,不能用普通本地文件或历史 `LEGACY_LOCAL` 记录充当默认安装源。 +5. 在“注册白名单”授权目标 TMS Agent ID,可增加架构、IP 和有效期约束;默认未授权 Agent 被拒绝。 +6. 返回“安装节点”,选择在线 Agent、节点地址和版本,点击“通过 TMS 安装 / 升级”。版本留空由后端按 RPM EVR 选择同架构最新版本;可明确选择历史版本。 +7. 在全局任务查看 `NODE_INSTALL` 的 Target 结果;安装成功后 Node 本地生成私钥和 CSR,自动注册并开始心跳。对已绑定 Agent 的重装复用 Node ID、轮换证书。 + +完成已发布 RPM、在线 TMS Agent 和注册白名单等前提后,从下列工作区操作。TMS Agent 必须先按 5.1 接入,安装 Node RPM 不负责把裸机自动变成 TMS Agent。截图未触发新的安装。 + +![Node RPM 标准安装源与已发布 Artifact](images/repository/repository-current-20261024-node-install-sources.png) + +![节点安装入口:选择 Agent、架构和 RPM 版本并填写地址](images/repository/repository-current-20261024-node-install.png) + +构建命令在 Repository Root 执行: + +```bash +VERSION=0.1.0 RELEASE=1 ARCHES="$(uname -m)" \ + OUTPUT_DIR="$PWD/output/repository-node-rpm" \ + bash scripts/repository-node-rpm.sh +``` + +`VERSION/RELEASE` 是实际发布版本;`OUTPUT_DIR` 是本地构建产物位置;默认 `BUILD_COMMIT` 来自当前 Git HEAD,若构建的是未提交源码则必须另外保存源码 manifest,不能仅凭 HEAD 声称构建一致。脚本只接受原生架构构建:分别在 OC9 `x86_64`、`aarch64` 执行,不能在 x86 上用一个 `ARCHES` 列表冒充已测试 ARM RPM。产物包含 RPM 和 Artifact 清单,不自动提交到 Git 或自动发布到产品仓库。 + +手工注册用于首个节点或已有证书的受控接入。登录 OCManager,进入“软件仓库 → 仓库节点 → 手动注册”,填写: + +| 字段 | 示例 | +| --- | --- | +| 节点名称 | `oc9-repository-node-01` | +| 控制地址 | `https://repo-node-01:19443` | +| 发布地址 | `http://repo-node-01:18090/repositories` | +| TLS server name | 节点证书 SAN 中的 DNS 名称 | +| 证书指纹 | Worker 客户端证书的 SHA-256 指纹 | + +创建后记录系统生成的 `storage_node_id`。节点启动时必须使用该 ID,不能临时生成另一个节点身份。 + +### 5.1 仓库节点与分发目标 Agent + +“仓库节点”和“分发目标节点”是两套独立注册数据,不能混用: + +| 对象 | 注册位置 | 数据来源 | 作用 | +| --- | --- | --- | --- | +| Repository Storage Node | “软件仓库 -> 仓库节点” | Footstone 的 `repository_storage_nodes` | 保存 RPM、生成 repodata、提供稳定 HTTP 源 | +| 分发目标 Agent | TMS/Agent 接入链路 | TMS asset-manager 与 heartbeat | 接收指定 immutable Revision,并回传状态、stdout/stderr 和 exit code | + +一台机器可以同时承担两种角色,但必须分别完成 Repository Node 注册和 TMS Agent 注册。分发任务的源是当前仓库通过 `primary_storage_node_id` 绑定的主仓库节点;“创建分发”弹窗中的目标列表由 `ListRepositoryDistributionNodes` 实时查询 TMS,不读取仓库节点列表。 + +Agent 安装包必须在安装前写入实际 Gateway 地址。为兼容不同版本的 `ocm-agent` 模板,不依赖模板中的默认地址或固定文本替换,统一从 OCManager 源码根目录执行: + +```bash +export AGENT_PACKAGE_DIR="$PWD/ocm-agent-package" +export GATEWAY_ADDR="management.example:8989" +bash scripts/configure-agent-package.sh "$AGENT_PACKAGE_DIR" "$GATEWAY_ADDR" +grep -A1 '^gateway_addrs:' "$AGENT_PACKAGE_DIR/config.yaml" +(cd "$AGENT_PACKAGE_DIR" && sudo bash install.sh) +``` + +分发脚本不要求 systemd 必须提供 `HOME`:优先读取可选的 `OCM_REPOSITORY_MIRROR_ROOT`,未设置时使用 `HOME`,再回退到当前运行用户的 passwd home。无需交互询问部署人的个人目录,避免无人值守部署被阻塞。若要把镜像放到独立数据盘,可在 Agent 服务中设置绝对路径并重启: + +```ini +[Service] +Environment=OCM_REPOSITORY_MIRROR_ROOT=/srv/ocmanager/repository-mirrors +``` + +新建一台虚拟机后,OCManager 不会扫描网络自动发现裸机。必须先在新节点安装 `ocm-agent`,让它通过 mTLS 连接控制节点的 TMS Gateway(`:8989`)。asset-manager 需显式配置: + +```yaml +registration: + auto_register: true +``` + +因此当前环境的接入流程为: + +```text +新节点安装并启动 ocm-agent +-> Agent 连接 :8989 +-> TMS 自动登记资产并分配 Agent ID +-> heartbeat 上报在线状态 +-> 重新打开分发弹窗后显示为可选目标 +``` + +在新节点检查: + +```bash +sudo systemctl enable --now ocm-agent +sudo systemctl status ocm-agent +ss -tnp | grep ':8989' +``` + +在控制节点确认资产和心跳: + +```bash +bash tms/deploy/scripts/gw-sign.sh -X POST \ + -H 'Content-Type: application/json' \ + -d '{}' \ + http://127.0.0.1:8080/api/v1/assets/query | + jq '.data.items[] | {ip, agent_id, status}' +``` + +Agent 完成首次注册后通常等待数秒即可在分发弹窗中看到;只有心跳在线的 Agent 可以选择。每台 Agent 必须使用自己的 UUID、MAC 和客户端证书,不能复制其他节点的证书或身份目录。 + +生产环境建议保持安全默认值 `auto_register: false`,先由管理员把 Agent 实际上报的 UUID、IP、MAC 加入白名单,再启动 Agent: + +```bash +bash tms/deploy/scripts/gw-sign.sh -X POST \ + -H 'Content-Type: application/json' \ + -H 'X-User-Name: admin' \ + -H 'X-User-Role: admin' \ + -d '{"uuid":"","ip":"","mac":""}' \ + http://127.0.0.1:8080/api/v1/assets/add | jq +``` + +Gateway 服务端证书的 SAN 应包含 Agent 实际访问的 Gateway IP 或域名。增加目标节点但继续使用同一个 Gateway 地址时无需重复签发服务端证书;只有 Gateway 对外地址变化时才需要更新 SAN。 + +## 6. 部署 Repository Node + +仓库节点配置示例: + +```dotenv +REPOSITORY_NODE_ID= +REPOSITORY_NODE_CONTROL_TARGET=ip://<管理节点>:19443 +REPOSITORY_NODE_SERVER_NAME=<管理节点证书SAN中的名称> +REPOSITORY_PUBLIC_BASE_URL=http://<仓库节点>:18090/repositories +REPOSITORY_DATA_HOST_DIR=/opt/ocmanager/repository-data +REPOSITORY_NODE_PKI_HOST_DIR=/etc/ocmanager/repository-node-pki +REPOSITORY_WORKER_VERSION=<冻结镜像版本> +``` + +| 配置 | 填写规则 | +| --- | --- | +| `REPOSITORY_NODE_ID` | 必须与控制台创建的 Storage Node UUID 一致 | +| `REPOSITORY_NODE_CONTROL_TARGET` | `ip://:19443`,主机地址可达且必须与证书 SAN 匹配 | +| `REPOSITORY_NODE_SERVER_NAME` | 管理面服务证书 SAN 中用于 TLS 验证的 DNS 名,不是页面显示名 | +| `REPOSITORY_PUBLIC_BASE_URL` | 该仓库节点对业务主机的只读 HTTP 根地址 | +| `REPOSITORY_DATA_HOST_DIR` | 宿主机 bind mount;默认 `/opt/ocmanager/repository-data`,更换时先检查所有者、空间、inode、rename 和 symlink | +| `REPOSITORY_NODE_PKI_HOST_DIR` | 节点私有证书目录;禁止放入 Repository 数据目录 | +| `REPOSITORY_WORKER_VERSION` | 与提交哈希或发布包一起冻结的 Worker 镜像标签,不使用未知变化的 `latest` | + +同步预算按 Include 自动选择:有 Include 时使用 `REPOSITORY_FILTERED_SYNC_BYTES/PACKAGES`,默认 `2 GiB / 1000 packages`;没有 Include 时会执行全量 `reposync`,使用 `REPOSITORY_MAX_SYNC_BYTES/PACKAGES`,默认 `64 GiB / 100000 packages`。管理员可在运行环境中分别覆盖,控制面与 Repository Node 必须配置一致;全量值同时作为筛选同步不能突破的硬上限。两组值不是仓库总容量限制,任务仍按 active、staging、新 Revision、metadata 和 20% 安全余量计算真实所需空间,并执行 90% 使用率与 inode 门禁。 + +节点 PKI 目录包含: + +```text +ca.crt +client.crt +client.key +node-server.crt +node-server.key +``` + +Repository Node 镜像使用预编译的静态 Worker。首次部署或源码版本变化后,先在源码根目录编译,再查看 Compose 展开结果,确认 worker 数据目录为读写挂载、Publisher 为只读挂载: + +```bash +make -C manager/backend build-bin-static +test -x manager/backend/bin/repository-worker + +export REPOSITORY_NODE_ENV_FILE="<节点私有环境文件>" +docker compose \ + --env-file "$REPOSITORY_NODE_ENV_FILE" \ + -f manager/deployments/compose/repository-node.compose.yml \ + config + +docker compose \ + --env-file "$REPOSITORY_NODE_ENV_FILE" \ + -f manager/deployments/compose/repository-node.compose.yml \ + up -d +``` + +回到控制台执行“立即探测”。节点应显示在线,并返回磁盘、inode、文件系统、工具版本和容量预检结果。 + +RPM 安装或重新配置 Repository Node 时,配置脚本会在 firewalld 已启用的系统上持久开放: + +```text +19443/tcp 管理面到 Repository Node 的 mTLS 数据接口 +18090/tcp 业务主机访问的 YUM/DNF 只读 HTTP 数据面 +``` + +若环境使用外部防火墙或安全组,仍需由部署人员开放相同端口。重启后应同时验证 `firewall-cmd --list-ports`、节点在线状态和 `repodata/repomd.xml` 的 HTTP 200,不能只检查进程存活。 + +Node 启动脚本对节点私有 Runtime 配置使用 `umask 077`,在启动 Worker 前恢复 `umask 022`。因此 Private Key 和 Runtime 配置保持私密,而 `repository-data/public/` 及其 Revision 父目录可由只读 Publisher 遍历。若 HTTP 返回 403,应先用 `namei -l` 检查整条公开路径;不要通过递归放宽 Repository Data 或 PKI 目录权限来规避问题。 + +### 6.1 单机本地 Compose 验证 + +管理面和 Repository Node 部署在同一台 OpenCloudOS 9 主机时,Footstone 内部控制端口默认占用 +`19443`。为避免节点数据服务使用同一宿主端口,在 Repository Node 私有环境文件中增加: + +```dotenv +REPOSITORY_NODE_CONTROL_TARGET=ip://<宿主机可达地址>:19443 +REPOSITORY_NODE_DATA_SERVER_PORT=19445 +REPOSITORY_PUBLIC_BASE_URL=http://<宿主机可达地址>:18090/repositories +``` + +此时在 OCManager 中登记的节点控制地址应为 `https://<本机可达地址>:19445`,发布地址为 +`http://<本机可达地址>:18090/repositories`。证书 SAN 必须包含这里实际使用的主机名或 IP。 +`REPOSITORY_NODE_SERVER_NAME` 与管理面证书 SAN 对应;容器内的 `127.0.0.1` 指向容器自身,不能当作宿主机控制地址。 +执行管理面 `scripts/deploy.sh up` 后,再使用上一节的 Repository Node Compose 命令启动节点。 +这种模式只用于本地复现;管理容器仍不挂载 Repository 数据目录,RPM 和 `repodata` 只由 +Repository Worker 与只读 Publisher 访问。 + +若启用自动安装,管理面的 Bootstrap 默认占用 `19444`,因此这里使用 `19445` 避免三条服务混用同一宿主端口。外部 Agent 访问时,发布地址不能使用只对本机有效的 `127.0.0.1`。 + +### 6.2 节点运维与任务配置 + +“仓库节点”列表由后端返回在线数、完整筛选集容量和严重告警数;Drawer 展示本节点全部 Repository、最近任务、预检与 Metadata Signing Key。页面的“当前内容大小”是 active Revision 的 RPM 逻辑总大小;“节点实际占用”来自文件系统采样,包含可归属的 Revision、staging、quarantine、metadata 和 trash。容量使用率来自节点文件系统,不能用包大小相加代替。采样时间过旧时应先探测节点,再做容量决策。 + +Drawer 的“同步”复用原有 Sync API、Mutation Lock、Worker 和 Atomic Publish;上游未配置、节点离线或已有同步任务时入口不可用。最近任务可跳转一级“任务总览”并保留节点筛选。任务总览支持 Repository/Node/类型/触发方式/状态及时间范围筛选,时间以 `created_at` 的 UTC/RFC3339 传输;顶部四态统计不受当前状态筛选影响。 + +仓库编辑表单可配置 MIRROR/APPEND、下载带宽和并行数;同步计划面板支持每日/每周、暂停和恢复,连续失败达到阈值自动暂停。调度使用唯一 Run Key 和 Task 幂等键,已有 Sync 时记录跳过而不制造重复失败任务。限速配置必须在对应 Node Worker 执行时生效,不能仅保存字段。 + +上传入口位于 Repository 的“软件包”页,支持多文件队列、每文件独立 Session、默认最多 2 个文件并发及按字节汇总进度。Indexed Chunk 最大 4 MiB,暂停、断网后只补缺失 Chunk;刷新页面后允许要求重新选择同一本地文件,不承诺浏览器自动保留 File。默认单文件上限仍为 `REPOSITORY_MAX_UPLOAD_BYTES=2 GiB`,需要更大文件时必须同时检查控制面/Node 的配置、展开上限和空间预算,不能因支持断点续传就假定大小无限。 + +软件包批量删除先 Preview 直接/传递反向依赖,并绑定 immutable Revision、摘要和过期时间;Confirm 后生成一个任务、逐包 Result Item 和一次 repodata rebuild。安装到 Agent 同样先按 Target Preview,再 Confirm 同一不可变 Revision;Revision 变化或 Preview 过期需重做预览。 + +Inventory 导出/导入用于定义和配置迁移。导入前重新映射节点与 Key 引用,Preview 后选择冲突策略再 Confirm;结果进入 `INVENTORY_IMPORT` 全局任务。它不恢复 RPM、Revision 内容、私钥、凭据或历史任务,完整备份仍使用第 9 节。 + +## 7. HTTP 发布验证 + +发布完成后,在“仓库详情 → 仓库配置 → Repo 配置”获取产品生成的真实内容,按需选择当前或固定发布版本。需要 GPG 时先完成授权公钥与节点元数据签名配置;不要照抄截图中的实验 baseurl。 + +![产品动态生成的固定发布版本 Repo 配置及 GPG 参数](images/repository/repository-current-20261024-config.png) + +使用已发布 Revision 的 `repomd.xml` 验证数据面: + +```bash +curl -I http://<仓库节点>:18090/repositories//current/repodata/repomd.xml +curl -X POST -o /dev/null -w '%{http_code}\n' \ + http://<仓库节点>:18090/repositories//current/repodata/repomd.xml +``` + +期望结果: + +```text +GET 200 +HEAD 200 +POST 403/405 +autoindex off +``` + +`.repo` 必须从 Repository Detail 的“Repo 配置”页面生成,不在测试脚本中另写 baseurl。 + +### 7.1 在独立 OC9 节点复现 GPG 安装 + +在包含当前源码的目录中创建可配置的演示根目录,再将页面下载的 `.repo` 放入其中: + +```text +./.run/repository-demo/repos/oc9-demo-private.repo +``` + +修复候选的演示脚本支持产品生成的单公钥和同一行空格分隔的多 `gpgkey` URL,包括包公钥与 Metadata 公钥不同的配置。逐个下载公钥并保存指纹,先在全新 installroot 内执行受限 `-y makecache` 准备元数据信任,再查询来源并真实安装;不改写产品配置或降低 `gpgcheck/repo_gpgcheck`。例如 `ocm-gpg-demo`,或指定已经补齐依赖闭包的 `bash`。执行前需要 OC9 的 dnf/repoquery、rpm、curl、gpg、jq、awk、基础 Shell 工具及 root/sudo 权限;最小 Worker 镜像不保证包含演示工具,缺少工具应按预检补齐,不能绕过。 + +```bash +cd +export OCM_REPOSITORY_DEMO_ROOT="${PWD}/.run/repository-demo" +export OCM_REPOSITORY_DEMO_REPO_FILE="${OCM_REPOSITORY_DEMO_ROOT}/repos/oc9-demo-private.repo" +export OCM_REPOSITORY_DEMO_PACKAGE=ocm-gpg-demo +sudo -v +make -C manager repo-gpg-install-demo +``` + +也可以不依赖 Makefile,直接调用: + +```bash +bash scripts/repository-gpg-install-demo.sh \ + --demo-root "${PWD}/.run/repository-demo" \ + --repo-file "${PWD}/.run/repository-demo/repos/oc9-demo-private.repo" \ + --package ocm-gpg-demo +``` + +脚本不会安装到演示节点的系统根目录。每次运行都使用新的 +`.run/repository-demo/runs/<时间戳>/installroot`,并将 Repository 来源、公钥信息、 +`repoquery`、DNF 日志、RPM 查询结果、结构化摘要和 SHA-256 清单保存在同一运行目录。 + +正式取证须同时检查 `makecache.log`、`dnf-exit-code.txt`、`rpm-query.txt` 和 `summary.json`,不能把准备元数据成功当成安装完成。新 installroot 需要仓库中存在目标包的依赖闭包,禁用系统默认源后缺依赖会明确失败。测试不会自动删除安装结果。 + +### 7.2 Repository Metadata 签名密钥 + +Metadata Signing 私钥只部署在实际承载 Repository 的节点,不写入 Git、MySQL、管理面环境 +变量、Inventory 或 Repository 公共数据目录。每个 Key 使用稳定且只包含字母、数字、点、 +下划线、加号和减号的 `key_ref`: + +```text +/etc/ocmanager/repository-metadata-signing//public.key +/etc/ocmanager/repository-metadata-signing//private.key +``` + +目录应由 Repository Node 服务账号可读,`private.key` 必须为 `0600` 或其他不向 group/other +开放的权限;`public.key` 可以为 `0644`。节点心跳只上报 `key_ref`、Fingerprint、创建时间、 +状态和公开文件名,不上传 Private Key 正文或私有路径。服务使用临时、权限为 `0700` 的 +`GNUPGHOME` 读取身份和执行签名,因此无需放宽 systemd 的只读根文件系统策略。 + +在控制台先确认“仓库节点 → 节点运维 → Repository Metadata 签名密钥”显示 Key 状态为 +`AVAILABLE`,再在 Repository 创建或编辑表单中选择该 `key_ref`。之后新发布的 Revision +应同时存在: + +```text +repodata/repomd.xml +repodata/repomd.xml.asc +keys/RPM-GPG-KEY-OCMANAGER- +``` + +只有签名、Fingerprint 和公开公钥 URL 均真实存在时,产品生成的 `.repo` 才会包含 +`repo_gpgcheck=1`;否则保持 `repo_gpgcheck=0`。切换 `key_ref` 只影响之后的新 Revision, +历史 immutable Revision 及其签名不得修改。部署后在独立 OC9 客户端下载产品生成的完整 `.repo`,使用临时 reposdir/installroot 禁用默认源验证(不修改系统现有源): + +```bash +read -r -p '产品生成的 .repo 文件路径: ' repo_file +run_dir=$(mktemp -d "${TMPDIR:-/tmp}/ocmanager-metadata-check.XXXXXX") +mkdir -p "${run_dir}/repos" "${run_dir}/installroot" +cp -- "${repo_file}" "${run_dir}/repos/ocmanager.repo" +repo_id=$(sed -n 's/^\[\([^]]*\)\]$/\1/p' "${run_dir}/repos/ocmanager.repo" | head -1) +test -n "${repo_id}" +set -o pipefail +sudo dnf -y --releasever=9 \ + --installroot="${run_dir}/installroot" \ + --setopt="reposdir=${run_dir}/repos" \ + --disablerepo='*' \ + --enablerepo="${repo_id}" \ + makecache 2>&1 | tee "${run_dir}/dnf-makecache.log" +printf '本次验证目录:%s\n' "${run_dir}" +``` + +命令保持 `.repo` 中的 `gpgcheck/gpgkey/repo_gpgcheck`,由 DNF 处理多个公钥 URL。`makecache` 验证元数据访问和签名,不等于 RPM 已安装;真实安装仍需选择该仓及其依赖可满足的包,并记录 DNF 退出码和 `rpm --root ... -q`,参见测试文档 Case A/B/C。验证目录留作证据,按需另行清理。 + +不要在命令行、普通日志或 Evidence 中输出私钥。备份 Repository 数据时,Metadata Signing +私钥应按独立 Secret/PKI 流程备份;Inventory Export 不承担私钥备份或恢复。 + +## 8. OCAI 部署边界 + +OCAI 使用官方 `ocai-service`。Repository 只向其发送脱敏后的失败摘要、稳定错误码和有限日志窗口。OCAI 输出原因、证据和建议,不具有仓库写权限,也不会自动重试任务。 + +```bash +bash scripts/deploy.sh ocai up +bash scripts/deploy.sh ocai ps +``` + +详细环境变量和独立组件说明见 [OCAI-DEPLOY.md](OCAI-DEPLOY.md)。OCAI 不可用时规则诊断继续工作,但 Enhanced Diagnosis 验收不能标记为 PASS。 + +部署完成后可单独验证 Repository 与 OCAI 的真实链路: + +```bash +export OCM_REPOSITORY_OCAI_BASE_URL="http://127.0.0.1:13071" +export OCM_REPOSITORY_OCAI_MODEL="deepseek-v4-flash" +export OCM_REPOSITORY_ADMIN_PASSWORD="" +make -C manager repo-ocai +``` + +该入口不会让 OCAI 执行仓库修改。它只读取脱敏的失败任务上下文并检查实际 SSE 响应,完整模型回答不写入 Evidence。 + +## 9. 重启、重建与恢复 + +正式部署文件中的管理面、基础设施、Repository Worker 和 Publisher 均使用 `restart: unless-stopped`。宿主机重启后应先确认 Docker 服务已启动,再检查容器健康状态;不要通过重新初始化数据库或删除 bind mount 来处理普通的容器未启动问题。历史 POC 或 disposable Evidence 栈可能保留 `restart: no`,这类环境需要按“数据库/Redis/ClickHouse → Footstone → 前端/OCAI → Repository Worker”的依赖顺序启动原容器。 + +### 9.1 Restart + +重启 API、worker、Publisher、TMS 和 OCAI 后,检查 active Revision、相对 `current` 链接、节点心跳和未完成任务 lease。 + +Repository Worker 重新领取因控制面中断而过期的任务时,会把上一 attempt 的 `execution.jsonl.zst` 与 metadata 移入 `task-logs//recovered/`,再创建当前 attempt 的日志。管理 API 始终读取任务目录下的当前日志,历史 attempt 仍随同一任务保留并执行相同的过期清理策略。恢复验收必须确认任务 attempt 增加、最终状态可收敛、旧日志仍存在,且没有 `TASK_LOG_INIT_FAILED`。 + +### 9.2 Redeploy + +重建应用容器时保留 MySQL 与 `/opt/ocmanager/repository-data`。重新挂载后检查 Repository ID、active Revision ID、manifest digest 和包清单。 + +### 9.3 Destroy/Restore + +只在 disposable 环境执行。使用 MySQL backup、Repository bind backup 和 manifest 恢复,再执行完整验收。不得对正式数据使用 `docker volume rm`、`git clean` 或未确认路径的递归删除。 + +自动恢复测试入口: + +```bash +sudo -v +make -C manager repo-recovery +make -C manager repo-acceptance +``` + +恢复 Compose 默认通过 `http://host.docker.internal:8080` 访问同机 TMS。TMS 位于其他主机、端口或反向代理路径时,不修改 Compose 文件,运行前同时设置以下两个变量: + +```bash +export P12_TMS_URL="http://:" +export P12_TMS_API_BASE="http://:/api/v1" +``` + +`P12_TMS_URL` 供恢复环境中的 Footstone 使用,`P12_TMS_API_BASE` 供 TMS 重启与 Agent 重连脚本使用,两者必须指向同一套 TMS 服务。 + +完整验收会在 disposable 环境中构建 RPM、启动临时数据库并执行 DNF/GPG 测试,运行账号需要 Docker 权限和临时 sudo 权限。通过 SSH 执行时应分配 TTY,在会话开始使用 `sudo -v` 交互授权;不要把 sudo 密码写入脚本、环境文件、命令行参数或 Evidence。无 TTY 且未预授权的会话会使报告、DNF 和 GPG 步骤失败,这属于运行前置条件未满足,不应复用该次失败结果。 + +若固定的 OC9 Worker 基础镜像站暂时无法解析,可以显式使用已经由当前源码构建并完成聚焦测试的本地镜像: + +```bash +export P12_REUSE_IMAGES=1 +export P12_FOOTSTONE_IMAGE=oc-footstone:phase12-recovery +export P12_WORKER_IMAGE=oc-repository-worker:phase12-recovery +make -C manager repo-acceptance +``` + +使用前必须记录镜像 ID、源码快照 SHA-256 和对应测试结果。该开关只处理外部镜像站不可用,不能复用旧的 DNF、GPG、OCAI、分发或验收汇总。 + +恢复脚本还会使用一个小型辅助镜像调整 disposable bind 目录属主。默认值为 `docker.io/library/alpine:3.19`;无法访问 Docker Hub 的内网环境应预先导入镜像,或显式指定可访问的等价镜像: + +```bash +export P12_HELPER_IMAGE=/library/alpine:3.19 +``` + +该变量只影响恢复测试临时目录的权限整理和销毁,不参与 Footstone、Repository Worker 或正式仓库数据面。若辅助镜像不可用,脚本必须按恢复失败退出,不能复用不完整 Evidence。 + +如果目标网络无法直连 `google.golang.org` 或 GitHub,完整验收中的 Go 集成测试可在当前进程指定可访问的 Go Module 代理: + +```bash +export GOPROXY=,direct +``` + +不要用全局 `go env -w` 改写评审机器。代理只负责获取 `go.mod/go.sum` 已锁定的依赖,不改变依赖版本;首次执行前仍应验证代理可达。Repository 的 RPM 测试夹具统一由 `.gitattributes` 强制 `*.spec text eol=lf`;从任意跨平台工作区制作交付包时使用 `git archive`,并在 OpenCloudOS 9 上运行发布检查,避免非 LF 换行被 `rpmbuild` 当作命令。 + +Repository Worker 默认基线固定为 OpenCloudOS 官方多架构镜像: + +```text +docker.io/opencloudos/opencloudos9-minimal:9.6-v20260714 +``` + +联网环境可提前执行 `docker pull`。隔离网应在部署前从可信环境导出该镜像,传入隔离网后执行 `docker load`,再核对镜像 ID/归档 SHA-256;也可以通过 `REPOSITORY_RUNTIME_IMAGE` 指向内部镜像仓库中的同一受控镜像。不要在部署过程中临时改用未经验证的其他发行版基础镜像。 + +## 10. 部署完成判定 + +基础控制面完成标准: + +- `scripts/check-environment.sh` 基础检查无失败。 +- infra、TMS、Footstone、MSG-ETL、Sysdiagnose 和前端均已启动。 +- `scripts/health-check.sh` 返回 0;未启用的 OCAI、Repository Node 和 Agent 不计为失败。 +- 登录、项目、主机和软件仓库入口可访问。没有 Repository Node 时,仓库 mutation 明确提示节点不可用。 + +完整软件仓库验收标准: + +- Footstone、TMS、Repository Worker 和 Publisher 状态正常;OCAI 仅在 Enhanced Diagnosis 验收时要求正常。 +- Repository Node 探测与容量预检 PASS。 +- 管理面容器不挂载持久化 RPM;控制节点宿主机兼任 Repository Node 时,RPM 仍只写入 Repository Node 的独立 bind 目录。 +- 上传和同步任务绑定正确 Storage Node。 +- `current` 指向不可变 Revision,重启后仍可读取。 +- 真实 `repomd.xml` 的 GET/HEAD 为 200,写方法被拒绝。 +- Case A/B/C、GPG 正负向、至少一个在线 Agent 的分发、诊断和恢复测试通过;配置多个 Agent 时还应验证多目标汇总。 +- `make -C manager repo-acceptance` 返回 0。 + +远程同步使用快捷包组时,应确认所选来源、版本和架构处于页面已核验范围。OpenCloudOS 9.6 已核验 `x86_64`、`aarch64` 和 `noarch` 的 BaseOS/AppStream 组合:前两种架构使用各自上游目录;`noarch` 使用固定的 x86_64 元数据入口,并由 worker 只下载和接收 noarch RPM。页面对未核验版本或自定义来源不提供猜测性包组,但仍允许输入上游实际存在的包名或 glob。设置 include 的任务在下载阶段即应用过滤,未设置 include 的完整同步则必须由容量预检和节点任务上限保证资源充足。 + +当前前端同时内置 OpenCloudOS 官方站和 `mirrors.opencloudos.tech` 国内镜像的 BaseOS/AppStream 来源。两组来源使用同一套版本、架构与包组约束;内置项只生成仓库创建参数,不写入数据库。现场网络访问官方站不稳定时可选择国内镜像,隔离网或新增镜像仍通过“上游源”页面登记地址模板,或在创建仓库时切换为“自定义地址”。 + +include 中部分包名不存在时,worker 会继续同步实际匹配到的包,并在任务日志中写入包含 `unmatched_packages` 的 `WARN`;全部包名均未匹配时返回 `SYNC_EMPTY_RESULT`,active Revision 保持不变。该行为要求 worker 使用项目生成的隔离 DNF 配置,不要用宿主机全局 `/etc/dnf/dnf.conf` 覆盖容器内任务配置。上传在 GPG 预检、归档检查、无包变更或用户取消阶段失败时,对应 Upload 必须收敛到 `FAILED`;超过 `expires_at` 的历史未完成上传不会继续阻断空仓库删除。 + +同一节点上提交多个同步任务时,后续任务可能等待较长时间。任务领取前会检查预检有效期;过期记录将使用原容量估算和节点最新状态刷新,刷新通过后继续执行,刷新失败则保留明确的磁盘、inode、工具或节点离线错误。无需为正常排队人工延长 TTL 或重新创建仓库。 + +最终结果以 [私有化软件仓库测试验收](私有化软件仓库测试验收.md) 和 [私有化软件仓库结构化验收报告](私有化软件仓库结构化验收报告.md) 为准。 + +### 10.1 含 GPG 公钥仓库的删除顺序 + +仓库删除仍采用显式清理流程,不级联删除正在发布的软件包和公钥。隔离项属于拒收内容,不参与发布,也不再阻断仓库删除;确认删除仓库时会一并删除该仓库的隔离元数据。对于已经发布过、但当前活动 Revision 已无软件包的仓库,按以下顺序操作: + +1. 在“软件包”页确认活动 Revision 的包数为 0;如仍有包,先通过按 NEVRA 删除生成空 Revision。 +2. 进入仓库详情的“仓库配置 → GPG 公钥与策略”删除已注册公钥。删除入口始终显示,但活动 Revision 仍含软件包时,后端会返回保护性错误。 +3. 返回仓库列表删除仓库,并按名称完成二次确认。隔离区即使仍有记录也可以继续删除。 + +公钥删除只移除数据库元数据、仓库节点上的授权公钥和公开副本,不涉及私钥。Build117 已覆盖“活动 Revision 有包时拒绝”和“活动 Revision 为空时允许”两条路径;Build118 增加隔离项不阻断仓库删除及隔离元数据事务内清理。 + +### 10.2 历史 Build141 增量部署记录 + +Build141 只替换 Footstone、Repository Worker 和前端静态资源,不引入数据库迁移或 RPC 协议变化。升级前保留当前镜像标签和 Repository bind backup,按“Footstone → 所有已注册 Worker → 前端”的顺序滚动更新;每步都应核对容器健康状态、节点心跳和当前 `repomd.xml` HTTP 200。升级后至少执行: + +```bash +# 后端专项回归 +(cd manager/backend && go test \ + ./services/footstone/repositoryworker \ + ./services/footstone/domain/repositoryService \ + ./services/footstone/domain/repositoryContentService \ + ./services/footstone/application) + +# 前端专项回归与正式构建 +(cd manager/frontend && npm run test:repository -- --reporter=dot) +(cd manager/frontend && npm run stylelint) +(cd manager/frontend && npm run build) +``` + +还需通过产品 API 或控制台各执行一次“有效包 + 不存在包”的混合同步、全部不存在包的失败同步,以及一个上传预检失败用例,分别确认 `SUCCESS + WARN`、`SYNC_EMPTY_RESULT` 和 Upload `FAILED`。这些是增量升级检查,不替代 `make -C manager repo-acceptance`。 + +### 10.3 提交前上游对齐 + +正式 PR 必须以最新上游分支为基线做合并验证,不能把本地目录整体复制到上游代码之上。推荐先在隔离 worktree 中预演,原工作区保持不变: + +```bash +git remote -v +git fetch +git diff --stat /master...HEAD +``` + +先从 `git remote -v` 确认哪个 remote 指向官方 `OpenCloudOS/ocmanager`,不能默认个人 fork 的 `origin/master` 就是最新上游。有未提交成果时先保存 binary patch、未跟踪清单和 SHA-256,在独立 candidate 目录以最新上游为基线受控应用改动,不直接在 dirty tree 中 merge/rebase。冲突必须查看两侧 diff,不能选择整侧覆盖。候选代码完成 Repository 后端测试、前端质量门禁、构建、发布检查与完整 Release Acceptance 后,才能进入 PR。历史 Build143 和 Build154 是此前的集成/部署证据,不替代下一次候选验证。 + +### 10.4 Build143 上游集成验收运行条件 + +Build143 使用 Git tree 生成候选归档,确保 Shell、SQL 和 RPM spec 均为 Linux LF。执行 `make -C manager repo-acceptance` 时,SSH 会话分配 TTY 并在当前会话交互完成 sudo 授权;JWT 从权限为 `0600` 的现有会话文件读取,密码、token 和 OCAI API Key 均未写入脚本或 Evidence。Docker Hub 不可达时,仅通过 `P12_HELPER_IMAGE` 和受控的已构建服务镜像处理外部镜像获取问题;Go 依赖获取则使用进程级 `GOPROXY`。 + +最终结果为 AC-01 至 AC-31 `31/31 PASS`,恢复、TMS 多目标增强用例、Case A/B/C、GPG、真实 OCAI、产品分发和 9 类 Evidence Bundle 全部 PASS。当时使用两个 Agent 是多目标能力的额外证据,不是后续环境必须固定部署两个 Agent。可追溯结果见测试验收文档中的 Build143 章节。 + +### 10.5 干净 OpenCloudOS 9 部署注意事项 + +2026-09-12 在新装 OpenCloudOS 9.6、空 Docker 数据目录和空 OCManager 数据卷上执行了从零部署。首次初始化必须等待 MySQL 的真实鉴权查询成功且 `@@port > 0`,不能把 `mysqladmin ping` 或容器 `healthy` 单独当作迁移就绪条件;否则可能在临时初始化 server 阶段触发 `ERROR 1045`。当前部署脚本已对该条件进行重试,并在迁移失败时停止后续应用启动。 + +受限网络需要分别处理三类下载:容器基础镜像由 `OCM_IMAGE_MIRROR` 等镜像变量控制,Alpine 构建阶段的软件包由 `APK_MIRROR` 控制,前端锁定依赖由 `NPM_REGISTRY` 控制。三者不是同一个服务,环境检查必须分别通过。推荐保持默认公开地址或替换为经过连通性验证的企业内网镜像,不要使用已经失效的旧域名。 + +部署完成后应确认实际启用组件的健康检查均通过,检查数量随组件及认证检查变化,不固定要求历史版本的 `17/17`。至少实际打开软件仓库列表、创建仓库弹窗、上游源和仓库节点页面。隐藏路由必须继承 `/repository/list` 的权限;管理员能够进入仓库列表却在上游源或仓库节点收到 403,属于路由权限配置错误,不是节点未部署。 + +## 11. 发布与运行检查 + +当前部署使用本章的 Compose 或 Node RPM、Repository Node 和只读发布方式。基础管理面可以在未部署 OCAI、Repository Node 或 Agent 时独立启动;依赖能力的验收分别由组件实际存在与否决定,不能把未运行的能力写成 PASS。 + +### 11.1 Node RPM 安装源 + +仓库节点安装包使用受保护的系统用途私有仓库发布,仍经过 RPM 上传、GPG 校验、Revision、`createrepo_c`、原子发布和 HTTP 只读访问。该仓库具有 `system_managed=true`、`purpose=NODE_RPM` 语义,不配置外部上游,不参与普通 MIRROR/APPEND 同步,也不能在仍被安装源引用时删除。 + +安装节点时,管理面按项目、启用的安装源、目标 Agent 架构和 RPM EVR 选择 Repository-backed Artifact;默认选择同架构最新 EVR,也允许明确选择历史版本。TMS 只下发小型安装脚本、临时仓库配置、指定 NEVRA 和 Enrollment 信息,目标机通过不可变 Revision URL 执行 `dnf install`。临时源继承仓库真实的 `gpgcheck`、`gpgkey` 和 `repo_gpgcheck`,禁止使用 `--nogpgcheck` 绕过策略,任务结束后清理临时配置。 + +### 11.2 注册授权与证书 + +Repository Node 自动注册采用项目级 Agent 白名单。`tms_agent_id` 是强身份,架构经过统一规范化后参与校验,主机名和 IP 只作为可选附加约束。创建 Enrollment 和消费 Bootstrap Token 时均检查白名单;Token 一次消费,普通重放被拒绝,只有相同授权 ID、CSR 摘要、项目、Agent 和架构的原请求可以从已持久化授权恢复证书签发。 + +节点私钥仅在节点本地生成和保存。管理面继续使用既有证书表作为公开证书权威来源,不保存节点私钥。禁用或删除白名单只阻止新的安装、Enrollment 和 Bootstrap,不自动下线已经注册的节点;需要停止在线节点时使用正式的“禁用节点”操作。 + +### 11.3 双架构与可选拓扑 + +Node RPM 支持 `x86_64` 和 `aarch64`。来自系统或 TMS 的 `amd64`、`x64`、`arm64` 等别名在后端统一转换为 canonical 值后再比较,前端不维护另一份架构映射。产品不要求固定两个 Agent:单 Agent 环境可以验证单目标安装和分发,多 Agent 环境用于验证一个 Task 下多个 Target 的独立结果汇总。 + +文档截图中的地址和节点数量仅表示验收时的实验拓扑,不是部署固定值。正式环境应通过配置文件、环境变量和控制台登记实际地址;仓库目录、证书目录和安装源均不得绑定开发机绝对路径。 + +### 11.4 部署后门禁 + +在 Repository Root 执行: + +```bash +bash scripts/repository-release-check.sh +make -C manager repo-acceptance-criteria +make -C manager repo-acceptance-results-test +``` + +三项分别检查发布文件、152 项预期集合和结果校验器自身的正负向行为,不执行 152 个实时产品用例。先按[测试验收](私有化软件仓库测试验收.md)收集当前源码对应的功能/安全/恢复证据,并提供机器结果文件,然后执行: + +```bash +export OCM_REPOSITORY_ACCEPTANCE_RESULTS="<本次验收结果JSON文件>" +make -C manager repo-acceptance +``` + +`repo-acceptance` 先实际执行恢复、旧核心 31 项矩阵和 9 类 Bundle,再严格校验所提供的 152 项结果。它不会自动为新环境生成全部 152 项结果,也不会把历史 JSON 当作当前运行证明。最终成功条件必须同时满足:当前源码与证据相符、命令退出码为 0、`failed_count=0`、预期和实际 ID 均为 152、没有重复/缺失/意外 ID、所有条目为 PASS。任何一项不满足都不能写成 `FINAL_READY=YES`。 + +### 11.5 独立修复候选的上线前检查 + +2026-10-23 的候选曾在独立测试环境完成组件更新与回归。部署前仍需记录现有镜像、运行配置、源码摘要和受影响文件差异;用唯一版本标签构建,仅更新确认过的服务,保留匹配的安全版本回滚。不覆盖未知 override、数据卷或其他工作树。2026-10-25 最新候选 `ddbdfea457644ed7d01e6c209e9dfd2168cd6953` 已同步到 Gitee `master` 和原 PR #420,PR 当前等待上游评审。 + +TMS HMAC 必须同时核对调用方 Footstone、sysdiagnose 和接收方 TMS API。使用部署人自己的受保护 Secret 文件或运行环境注入同一个非空 `TMS_API_GW_HMAC_SECRET`,正式接收方保持 `TMS_API_GW_HMAC_STRICT=true`;配置文件权限限运行账号,禁止打印完整 `docker compose config`、容器环境或 Secret 内容。不要复制实验机的 Secret 路径作为部署默认。 + +已有环境切换时先使调用方具备签名能力,再启用接收方严格认证;保留受控窗口与只读验证。运行 `bash scripts/health-check.sh` 时,新增检查会识别接收方空 Secret、非严格模式和未签名请求被接受的漂移;缺少 TMS API 容器时不会伪报该组件已通过。健康检查不替代真实合法请求与篡改/重放负向测试。失败回滚必须恢复相互匹配的安全配置,不将关闭 STRICT 作为正式解决办法。 + +节点 30 秒无有效心跳后不应继续显示 ONLINE;独立维护和列表/详情读取都会刷新陈旧状态,最近容量仍保留,不代表实时容量。Worker 控制连接中断时保留任务历史、租约与 fencing,不手删锁或任务;控制面可用后按既有租约和每 10 秒维护周期恢复,公网下载速度不在此时间承诺范围内。 + +### 11.6 界面候选的产物替换与回滚 + +2026-10-23 在现有 OC9 测试环境替换了前端、Footstone 与 RPM/systemd 运行的 Worker,并完成了该轮产物回归。测试环境使用独占候选镜像标签,前端仅替换静态目录、Footstone 仅替换服务二进制;既有 Compose 文件顺序、挂载、认证 Secret 和证书保持。Worker 原文件备份后以同目录原子重命名替换,保留权限和 SELinux 标签,再由 systemd 重启。2026-10-25 最新候选已同步到 Gitee `master` 和原 PR #420,PR 等待上游评审。 + +复现时应先确认当前生效路径、镜像及配置差异并保存 SHA-256 和回滚引用,确认没有进行中任务后更新明确服务。使用自己的新版本标签和临时目录,不照抄实验环境路径。前端部署后需刷新已打开的旧页面,并核对实际加载的资源 hash;仅运行 Vite 不能证明正式静态产物已更新。 + +演示工具与 Worker 运行依赖不同。最小 Worker 镜像可能缺少 `jq` 或 `awk`;运行安装演示脚本前须按其预检补齐 `jq`、`gawk` 及基础命令。隔离测试容器可以从 OC9 系统源安装这些测试工具,但业务安装仍使用产品生成的 immutable `.repo`、禁用默认源,并保持 `gpgcheck=1/repo_gpgcheck=1`,不能通过降低校验绕过。 + +替换后检查实际产物 SHA、服务健康、有效节点心跳和 HMAC 一致性,再执行[测试文档第 13 节](私有化软件仓库测试验收.md#13-界面统一与已部署候选的联验)。本次实际结果见[上线回归摘要](evidence/repository-style-validation-20261023.json)。共享运行环境不要直接执行含停启和 destroy/restore 的 `repo-acceptance`;完整发布门禁仍须按 11.4 在受控验收环境进行。 + +2026-10-24 恢复实验补充:Worker 需要包含取消执行日志状态一致性修正,不能只更新管理面。节点任务默认租约 60 秒,任务续租间隔约 20 秒,控制面独立维护周期 10 秒;故障恢复应以任务实际 lease、扫描、RPC 和节点恢复时间计量,不承诺任意下载都在固定秒数内完成。执行故障实验必须先确认无其他在途任务,准备恢复 finally,并使用专属小型测试仓;不要在正常运行环境直接套用全组件停启脚本。 + +### 11.7 当前候选的补验前提 + +Node RPM helper、systemd unit、Nginx 配置和应用模板必须按 `.gitattributes` 使用 LF,不能将 Windows CRLF shebang 原样打包。双架构使用同一源码清单原生构建,核对架构、有效载荷、SHA-256 和真实签名;未提交候选必须另存源码清单,不能仅凭 Commit 相同判断产物一致。 + +TMS 恢复脚本复用 `tms/deploy/scripts/gw-sign.sh` 对健康检查、JSON 和 multipart 实际字节签名。执行前在私有环境导出与接收方一致的 `TMS_API_GW_HMAC_SECRET`,缺失则失败,不能关闭 STRICT。多个追加 override 通过 `OCM_REPOSITORY_TMS_ADDITIONAL_OVERRIDE_FILES` 按原顺序逐行指定;检查仅用 `docker compose config --quiet`,不打印插值配置。 + +环境检查必须按白名单读取非敏感字段,禁止输出全部环境、完整容器配置或浏览器 Token。意外输出认证材料后,应停止收集、排除该输出进入 Git/Evidence,并轮换受影响凭据。隔离 ARM/NAT 实验的临时通路不是产品默认;安装时 firewalld 重载可能使临时规则失效,应重新核验网络,不能跳过 TLS/GPG 或修改任务数据库来绕过。 + +多文件队列与安装版本候选修复属于前端静态产物,部署后应核对浏览器实际加载的资源与 dist 文件 SHA,不能只确认源文件已更新。切换安装源会保留历史 Artifact 记录,但安装下拉只允许当前有效源;历史版本应先通过同一标准仓库上传、GPG 校验、不可变发布和登记,再由页面选择。验收用旧版本号 fixture 不等于恢复了对应历史代码,也不应为界面验收随意降级正在承载业务的仓库节点。 + +`scripts/tests/mysql_migration_upgrade_test.sh` 除隔离数据库 SQL 测试外,还通过实际 `deploy.sh` 迁移函数验证 v17 到 v18 的自动发现、应用和重入。只将 Compose 执行适配到测试容器,不调用部署 `main`,不操作实际部署数据库;它不是一次完整的全组件从零部署。 + +### 11.8 认证轮换与验收数据库隔离 + +本次独立候选已完成外部 OCAI Key 与内部 TMS HMAC、共享 JWT、MySQL/Redis/ClickHouse 凭据轮换。外部与内部按阶段记录,早期外部摘要中的内部 `PENDING` 是当时状态;最新内部结果见[认证轮换摘要](evidence/repository-internal-authentication-rotation-20261024.json)。实际数据库认证值与调用方配置一同切换,旧凭据拒绝、新凭据可用,原镜像、挂载、卷及 Repository/Task/Revision 指纹保持。后续真实登录、双架构 Node 心跳与 OCAI 分析的结果见[测试验收 15.2](私有化软件仓库测试验收.md#152-认证轮换后的产品复查)。这些是本次测试环境记录,不是可直接照抄的生产密钥或部署默认。 + +新部署仍由部署人生成自己的独立强 Secret,保持 TMS 调用方和接收方一致、JWT 签发与校验方一致。已有数据库轮换必须同时更新真实数据库账号与所有使用方,不能只改 `.env`;先留仓库外受保护备份、确认空闲窗口和回滚配置,再验证新旧凭据、持久化数据和真实产品链路。禁止打印完整环境或 Compose 插值配置。当前 ZooKeeper 未启用认证,本次不宣称已通过密码变量启用 ZooKeeper 身份校验。 + +发布验收的临时数据库与部署数据库是两件事。`repository-report.sh`、`repository-e2e.sh`、`repository-diagnosis.sh`、`repository-gpg.sh` 可以使用[测试验收 15.1](私有化软件仓库测试验收.md#151-独立容器-mysql-模式)的独占高端口容器;不要把部署 MySQL 的地址、密码或容器名传给测试 helper。未提供外部测试数据库参数时仍按原 socket/MariaDB 和 sudo 方式执行;新增模式不改变产品的部署架构。 + +实际保护仓 API 已验证同步、确认删除及不匹配 Artifact 均被拒绝,并返回统一 Code、中文原因和建议,见[错误映射验证](evidence/repository-node-rpm-error-validation-20261024.json)。遗漏删除确认时仍返回英文参数错误,这一边界没有被写成已经修复。这一专项复查时点的完整发布入口尚未结束,当时为 `FINAL_READY=NO`;最终结果另见 11.9,不因服务健康或轮换成功自动通过。 + +### 11.9 冻结候选的最终部署验收 + +2026-10-24 在 OpenCloudOS 9 对冻结候选实际执行 `make -C manager repo-acceptance`,退出码为 0;核心/增强矩阵 31/31、Evidence Bundle 9/9、全部 11 个执行步骤及严格 152 项校验均通过。预期、唯一预期、实际、唯一实际均为 152,重复、缺失、意外、非 PASS 和不完整证据条目均为 0。结果见[最终候选摘要](evidence/repository-final-validation-20261024.json)、[152 项机器结果](evidence/repository-acceptance-progress-20261024.json)和[完整发布验收](evidence/repository-release-validation-20261024.json)。 + +```text +152/152 PASS +OVERALL_ACCEPTANCE = PASS +FINAL_READY = YES +``` + +全量运行发现的 GPG 临时密钥用例使用了过长 Unix socket 路径,已只在测试夹具采用受控短临时目录并完整重跑;隔离运行器同步固定工具路径和最小环境注入。此收口没有修改 Repository 业务链路或放松 GPG/mTLS/Mutation Lock。源码清单、锁文件和运行产物指纹与证据绑定,P14 只更新文档与证据链接。 + +本次验证复用已开启的 OC9 测试机器;单 Agent、多 Agent、单节点多仓与双架构证据分别保留,不将实验机器名、临时 NAT 路由或固定测试地址写成部署前置条件。完整验收包含独立测试数据库、重启、重新部署、销毁恢复和真实 TMS 重启,测试数据库不连接部署数据库。 + +验收范围继续保持明确:ZooKeeper 的历史部署未启用认证,本轮未扩展 SASL;Repository 定向类型检查通过不等于全站历史 TypeScript 问题已清零;旧版本号 RPM 已验证页面选择与 EVR 策略,不声称对运行节点执行过历史代码降级;TMS Agent 需先按既有 TMS 部署方式安装并在线,本轮自动安装的是 Repository Node RPM,不是为新机器自动引导部署 TMS。遗漏删除确认的普通参数错误仍可能返回英文技术提示,不能据此宣称所有参数校验均已中文化。 diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index 5928aa4c0ae06ffb53af10e38156f64774f606d1..99f50f80d9532927653c0a4ea3eedb8bc3a76a1e 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -9,9 +9,7 @@ # # 推荐:经由顶层一键脚本(自动按顺序拉 infra → tms → manager) # bash scripts/deploy.sh up # -# 基础镜像默认经 mirror.ccs.tencentyun.com(Docker Hub 的腾讯云加速;daemon.json 里常写为 -# https://mirror.ccs.tencentyun.com)。在腾讯云 CVM 等环境通常最稳;非腾讯云或拉取失败时可改 -# docker.io 短名或本机 registry-mirrors。 +# 基础镜像默认从 Docker Hub 拉取;隔离网可通过 OCM_IMAGE_MIRROR 指向内网 registry。 # # # 手动分步 # docker compose -f infra/docker-compose.yml --env-file config/.env up -d @@ -26,7 +24,7 @@ name: ocmanager-infra services: mysql: - image: ${OCM_IMAGE_MIRROR:-mirror.ccs.tencentyun.com}/library/mysql:${MYSQL_VERSION:-8.0.35} + image: ${OCM_IMAGE_MIRROR:-docker.io}/library/mysql:${MYSQL_VERSION:-8.0.35} container_name: oc-mysql restart: unless-stopped environment: @@ -48,6 +46,16 @@ services: - ../manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_2_init_data.sql:/docker-entrypoint-initdb.d/02_footstone_data.sql:ro - ../manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_3_metrics_meta.sql:/docker-entrypoint-initdb.d/03_footstone_metrics.sql:ro - ../manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_4_metrics_meta_seeds.sql:/docker-entrypoint-initdb.d/04_footstone_metrics_seeds.sql:ro + - ../manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_5_software_repositories.sql:/docker-entrypoint-initdb.d/05_footstone_repositories.sql:ro + - ../manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_6_repository_tasks.sql:/docker-entrypoint-initdb.d/06_repository_tasks.sql:ro + - ../manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_7_repository_content.sql:/docker-entrypoint-initdb.d/07_repository_content.sql:ro + - ../manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_8_repository_publication.sql:/docker-entrypoint-initdb.d/08_repository_publication.sql:ro + - ../manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_9_repository_diagnosis.sql:/docker-entrypoint-initdb.d/09_repository_diagnosis.sql:ro + - ../manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_10_repository_distribution.sql:/docker-entrypoint-initdb.d/10_repository_distribution.sql:ro + - ../manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_11_repository_gpg_keys.sql:/docker-entrypoint-initdb.d/11_repository_gpg_keys.sql:ro + - ../manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_12_repository_storage_nodes.sql:/docker-entrypoint-initdb.d/12_repository_storage_nodes.sql:ro + - ../manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_13_repository_task_completion.sql:/docker-entrypoint-initdb.d/13_repository_task_completion.sql:ro + - ../manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_14_repository_upstream_profiles.sql:/docker-entrypoint-initdb.d/14_repository_upstream_profiles.sql:ro - ../manager/backend/migrations/sysdiagnose/sql/mysql/v1.0.0.0/v1.0.0_1_init_tables.sql:/docker-entrypoint-initdb.d/10_sysdiagnose.sql:ro healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_ROOT_PASSWORD:-oc_manager}"] @@ -58,7 +66,7 @@ services: - oc-net redis: - image: ${OCM_IMAGE_MIRROR:-mirror.ccs.tencentyun.com}/library/redis:${REDIS_VERSION:-7.2.3}-alpine + image: ${OCM_IMAGE_MIRROR:-docker.io}/library/redis:${REDIS_VERSION:-7.2.3}-alpine container_name: oc-redis restart: unless-stopped # 当 REDIS_PWD 为空时不启用密码(默认开源/本机部署,避免 tms 配置不一致); @@ -91,7 +99,7 @@ services: - ocm-redis zookeeper: - image: ${OCM_IMAGE_MIRROR:-mirror.ccs.tencentyun.com}/${ZOOKEEPER_REPO:-bitnami/zookeeper}:${ZOOKEEPER_VERSION:-3.8.3} + image: ${OCM_IMAGE_MIRROR:-docker.io}/${ZOOKEEPER_REPO:-bitnamilegacy/zookeeper}:${ZOOKEEPER_VERSION:-3.8.3} container_name: oc-zookeeper restart: unless-stopped environment: @@ -113,7 +121,7 @@ services: - oc-net kafka: - image: ${OCM_IMAGE_MIRROR:-mirror.ccs.tencentyun.com}/${KAFKA_REPO:-bitnami/kafka}:${KAFKA_VERSION:-3.5.1} + image: ${OCM_IMAGE_MIRROR:-docker.io}/${KAFKA_REPO:-bitnamilegacy/kafka}:${KAFKA_VERSION:-3.5.1} container_name: oc-kafka restart: unless-stopped depends_on: @@ -143,7 +151,7 @@ services: - ocm-kafka clickhouse: - image: ${OCM_IMAGE_MIRROR:-mirror.ccs.tencentyun.com}/clickhouse/clickhouse-server:${CLICKHOUSE_VERSION:-23.8.8.20} + image: ${OCM_IMAGE_MIRROR:-docker.io}/clickhouse/clickhouse-server:${CLICKHOUSE_VERSION:-23.8.8.20} container_name: oc-clickhouse restart: unless-stopped ulimits: diff --git a/manager/Makefile b/manager/Makefile index ce321af10af7a8d3050dc87c8018c76793912724..dca29b8c7c5db1df4686965dfa3e7130eecd1ba6 100644 --- a/manager/Makefile +++ b/manager/Makefile @@ -2,11 +2,11 @@ # 开源文档与路线图见仓库根目录 README.md、docs/ # 使用:cd manager && make # -# 本机 Node.js < 18 时前端会失败(Vite 6 要求)。任选其一: +# 本机 Node.js < 20 时前端会失败(前端依赖基线要求)。任选其一: # SKIP_FRONTEND=1 make # 只编后端 -# nvm use 18 && make # 或升级/切换 Node 后再全量编译 +# nvm use 20 && make # 或升级/切换 Node 后再全量编译 -.PHONY: all backend frontend clean deploy-help images images-backend +.PHONY: all backend frontend clean deploy-help images images-backend repo-e2e repo-diagnosis repo-log-governance repo-ocai repo-report repo-gpg repo-gpg-install-demo repo-distribution repo-recovery repo-evidence-bundle repo-release-check repo-basic-acceptance repo-acceptance-matrix repo-acceptance-criteria repo-acceptance-results-test repo-acceptance all: backend ifneq ($(SKIP_FRONTEND),1) @@ -32,3 +32,69 @@ images: images-backend: $(MAKE) -C deployments images-backend + +# Phase 7:在 OpenCloudOS 9 上执行三个隔离的 DNF 安装用例。 +repo-e2e: + bash ../scripts/repository-e2e.sh + +# Phase 8:验证确定性失败诊断、持久化、API 映射与报告导出。 +repo-diagnosis: + bash ../scripts/repository-diagnosis.sh + +# P4/P9:验证 GB 级日志流式压缩、摘要上限、脱敏、索引和只读下载。 +repo-log-governance: + bash ../scripts/repository-log-governance.sh + +# Phase 8/11:验证 OCAI 健康状态、上下文边界、脱敏和真实模型 SSE 分析。 +repo-ocai: + bash ../scripts/repository-ocai.sh + +# Phase 11:通过产品服务导出 inventory、validation 和 distribution JSON。 +repo-report: + bash ../scripts/repository-report.sh + +# Phase 10/11:生成临时签名材料并验证 GPG 正向、负向闭环,私钥不会进入 Evidence。 +repo-gpg: + bash ../scripts/repository-gpg.sh + +# 现场复现:读取产品生成的 .repo,在全新 installroot 中只启用该仓库并真实安装签名 RPM。 +repo-gpg-install-demo: + bash ../scripts/repository-gpg-install-demo.sh + +# Phase 9/11:查询已注册 Agent,验证 immutable revision 从仓库节点分发到独立目标节点。 +repo-distribution: + bash ../scripts/repository-distribution.sh + +# Phase 12:在 disposable 环境验证 restart、redeploy、destroy/restore 与 TMS 重启。 +repo-recovery: + bash ../scripts/repository-recovery.sh + bash ../scripts/repository-tms-recovery.sh + +# P9:把 31 项追踪矩阵聚合为评审使用的 6 个 Core 与 3 个 Enhanced Evidence Bundle。 +repo-evidence-bundle: + bash ../scripts/repository-evidence-bundle.sh + +# 提交前只读自检:检查临时产物、密钥、实验路径、脚本语法、JSON 和文档图片。 +repo-release-check: + bash ../scripts/repository-release-check.sh + +# 基础部署档位:验证可选组件不会成为隐式依赖,并检查当前基础服务健康状态。 +repo-basic-acceptance: + bash ../scripts/tests/deploy_optional_components_test.sh + bash ../scripts/health-check.sh + +# 执行并聚合完整 AC-01 至 AC-31 矩阵;包含 Core 与 Enhanced 项。 +repo-acceptance-matrix: + bash ../scripts/repository-acceptance.sh + +# P9:校验第三阶段权威 152 项验收集合及验证器正负向行为。 +repo-acceptance-criteria: + bash ../scripts/repository-acceptance-results.sh --criteria-only + +repo-acceptance-results-test: + bash acceptance/tests/repository-acceptance-results-test.sh + +# P13/P14 正式门禁:先执行旧核心矩阵,再要求第三阶段 152 项全部 PASS。 +repo-acceptance: + bash ../scripts/repository-release-acceptance.sh + bash ../scripts/repository-acceptance-results.sh --strict diff --git a/manager/acceptance/repository-acceptance-criteria.json b/manager/acceptance/repository-acceptance-criteria.json new file mode 100644 index 0000000000000000000000000000000000000000..0b0c316e302be98c43ddd4fa871ff292a8ab1478 --- /dev/null +++ b/manager/acceptance/repository-acceptance-criteria.json @@ -0,0 +1,918 @@ +{ + "schema_version": 1, + "expected_count": 152, + "items": [ + { + "id": "1", + "title": "软件仓库主页面布局完成重排:保持现有配色、组件库、字体、交互风格,不更换整体 UI 风格,不引入新的前端框架、字体或色板。", + "category": "UI", + "required": true + }, + { + "id": "2", + "title": "主页面查询区域重新排版:查询条件宽度合理、控件高度统一、Label 对齐,消除右侧大面积无意义空白;窄屏时能够自然换行,不出现按钮挤压和输入框异常拉伸。", + "category": "UI", + "required": true + }, + { + "id": "3", + "title": "仓库列表列宽重新分配:仓库名称列不再过宽,其余字段不再严重挤压;长名称使用省略号,并可通过 Tooltip 查看完整内容。", + "category": "UI", + "required": true + }, + { + "id": "4", + "title": "仓库列表取消低频“创建人”核心列:创建人等低频信息移入详情或次级信息展示,不再长期占据主列表空间。", + "category": "UI", + "required": true + }, + { + "id": "5", + "title": "仓库列表新增高频运维字段:必须直接展示包数量、仓库总体积、最近同步时间,不需要进入仓库详情才能查看。", + "category": "UI", + "required": true + }, + { + "id": "6", + "title": "仓库列表字段视觉统一:数字、时间、状态、标签等对齐合理;同类字段宽度稳定,不因内容长短造成明显跳动。", + "category": "UI", + "required": true + }, + { + "id": "7", + "title": "解决“系统/产品+版本分组”和后端分页冲突:不能继续出现某个分组在当前页只剩一行、视觉上被分页截断的情况。", + "category": "UI", + "required": true + }, + { + "id": "8", + "title": "主列表分页方案最终确定并实现:优先采用平铺分页表格,将系统/产品和版本作为明确字段或标签展示,通过筛选实现逻辑分组;不得为了视觉分组破坏现有后端分页能力。", + "category": "UI", + "required": true + }, + { + "id": "9", + "title": "新增一级“任务总览”页面:任务页面不能再只能从单仓库详情进入。", + "category": "TASK", + "required": true + }, + { + "id": "10", + "title": "一级菜单层级调整完成:至少形成“软件仓库 → 任务总览 → 仓库节点 → 上游源”的一级导航结构;上游源原有页面内容不做无必要改版。", + "category": "TASK", + "required": true + }, + { + "id": "11", + "title": "任务总览覆盖所有仓库任务:能够聚合所有仓库、所有节点相关任务,而不是仅显示当前仓库任务。", + "category": "TASK", + "required": true + }, + { + "id": "12", + "title": "任务总览顶部有统计信息:至少显示等待中、执行中、失败、成功数量;点击对应统计项能够直接应用状态筛选。", + "category": "TASK", + "required": true + }, + { + "id": "13", + "title": "任务总览支持完整筛选:至少支持任务状态、任务类型、仓库、节点、触发方式和时间范围筛选。", + "category": "TASK", + "required": true + }, + { + "id": "14", + "title": "任务触发来源明确:至少区分人工触发、定时触发、系统触发;如果存在自动重试,也应单独标识。", + "category": "TASK", + "required": true + }, + { + "id": "15", + "title": "任务列表按时间顺序合理排列:默认优先展示最新任务,通常按创建或开始时间倒序。", + "category": "TASK", + "required": true + }, + { + "id": "16", + "title": "任务列表显示节点来源信息:活动任务不再缺少来源节点或执行节点信息。", + "category": "TASK", + "required": true + }, + { + "id": "17", + "title": "失败任务一眼可见:失败状态使用项目已有错误状态视觉,不额外创建新色板。", + "category": "TASK", + "required": true + }, + { + "id": "18", + "title": "失败原因无需多层点击:失败摘要应直接出现在任务列表或一级展开区域,用户不需要连续打开两层详情才能知道失败原因。", + "category": "TASK", + "required": true + }, + { + "id": "19", + "title": "任务详情仍可展开查看完整信息:完整日志、错误码、上下文等可通过 Drawer 或详情区域查看。", + "category": "TASK", + "required": true + }, + { + "id": "20", + "title": "仓库节点提升为一级页面:节点不再藏在单个仓库页面内,也不再依赖“进去再返回”的导航方式。", + "category": "NODE", + "required": true + }, + { + "id": "21", + "title": "节点页顶部提供集群容量概览:至少展示节点总数、在线节点数、集群总容量、已使用容量和告急节点数。", + "category": "NODE", + "required": true + }, + { + "id": "22", + "title": "节点列表直接展示部署仓库情况:用户必须能直接知道某台机器上部署了多少仓库,并看到仓库概要信息。", + "category": "NODE", + "required": true + }, + { + "id": "23", + "title": "节点详情能够看到完整仓库清单:每个仓库至少显示仓库名称、系统/版本、仓库类型、空间占用和状态。", + "category": "NODE", + "required": true + }, + { + "id": "23A", + "title": "节点详情必须形成以仓库节点为主视角的 Repository/Source 列表:对于当前节点承载的每个仓库,至少直接展示仓库名称、系统/产品、版本、仓库类型、架构、包数量、当前 Revision、最近同步时间、最近同步状态,以及下次定时同步时间(如已配置)。不能只显示仓库名称和简单状态。", + "category": "NODE", + "required": true + }, + { + "id": "23B", + "title": "节点视角必须明确区分仓库“内容大小”和“实际占用”:logical_bytes 表示当前发布 Revision 中的软件包逻辑总大小,physical_bytes 表示该仓库在当前节点上的真实磁盘占用,可包括 immutable revisions、staging、quarantine、metadata 等;这两个指标不得混用,核心统计数据必须由后端/节点真实统计提供,不能由前端临时估算。", + "category": "NODE", + "required": true + }, + { + "id": "24", + "title": "节点存储容量直接显示在主列表:至少展示已用容量、总容量和使用率,不能再必须打开详情才能查看。", + "category": "NODE", + "required": true + }, + { + "id": "25", + "title": "节点容量实现四级预警:至少分为“容量充足、容量适中、容量偏高、容量告急”四档。", + "category": "NODE", + "required": true + }, + { + "id": "26", + "title": "容量状态颜色递进:四档状态必须使用当前项目已有 success/info或primary/warning/danger 等 Token,不能新建一套颜色体系。", + "category": "NODE", + "required": true + }, + { + "id": "27", + "title": "容量阈值集中维护:不得在多个 Vue 文件中散落 Magic Number;默认可采用 <60%、60%-75%、75%-90%、>=90%,如已有产品配置则以统一配置为准。", + "category": "NODE", + "required": true + }, + { + "id": "28", + "title": "节点详情采用一级页面 + Drawer 方案:不再新增独立二级 Node Detail Page;节点完整详情通过右侧 Drawer 展示。", + "category": "NODE", + "required": true + }, + { + "id": "29", + "title": "节点离线原因直接显示:不能只显示“离线”两个字;状态附近至少显示最近一次错误摘要或错误码,并显示最近心跳时间。", + "category": "NODE", + "required": true + }, + { + "id": "30", + "title": "仓库概览页完成信息层级重排:不能继续十几个键值对平铺。", + "category": "UI", + "required": true + }, + { + "id": "31", + "title": "仓库概览第一层突出核心信息:当前发布版本/Revision、发布地址、主节点在线状态必须位于最明显区域。", + "category": "UI", + "required": true + }, + { + "id": "32", + "title": "仓库概览第二层展示关键指标:至少展示包数量、总体积、Revision 数量、隔离区待处理数量。", + "category": "UI", + "required": true + }, + { + "id": "33", + "title": "低频配置下沉:仓库类型、架构、GPG 策略、上游地址、同步模式等进入下方配置区域,不与发布状态等核心信息等权展示。", + "category": "UI", + "required": true + }, + { + "id": "34", + "title": "仓库详情 Tab 数量明显收敛:上传和隔离区不得继续各自占用独立 Tab。", + "category": "UI", + "required": true + }, + { + "id": "35", + "title": "上传功能移动到软件包页面:通过“上传软件包”按钮打开 Dialog 或 Drawer 操作。", + "category": "UI", + "required": true + }, + { + "id": "36", + "title": "隔离区移动到软件包页面:通过按钮进入;按钮必须带待处理数量 Badge,有待处理软件包时可以一眼发现。", + "category": "UI", + "required": true + }, + { + "id": "37", + "title": "GPG 配置和 Repo 配置合理合并:不再分别占据两个低频一级 Tab,可统一归入配置区域。", + "category": "UI", + "required": true + }, + { + "id": "38", + "title": "软件包列表字段增强:至少包含包名、Version、Release、Architecture、Size、签名状态、所属 Revision、入库时间、SHA256。", + "category": "UI", + "required": true + }, + { + "id": "39", + "title": "软件包列表支持架构筛选。", + "category": "UI", + "required": true + }, + { + "id": "40", + "title": "软件包列表支持签名状态筛选。", + "category": "UI", + "required": true + }, + { + "id": "41", + "title": "软件包列表提供“只看未签名”快捷筛选。", + "category": "UI", + "required": true + }, + { + "id": "42", + "title": "Repo 配置进入页面后自动生成:用户不再需要额外点击 Generate。", + "category": "UI", + "required": true + }, + { + "id": "43", + "title": "current 与 immutable revision 切换时自动刷新 Repo 配置。", + "category": "UI", + "required": true + }, + { + "id": "44", + "title": "Generate 按钮移除:保留刷新、复制、下载等实际需要操作。", + "category": "UI", + "required": true + }, + { + "id": "45", + "title": "支持按系统/产品 + 版本批量生成 Repo 配置:例如选择 OpenCloudOS 9.6 后能够一次得到 BaseOS、Updates、Private 等多个仓库的配置。", + "category": "UI", + "required": true + }, + { + "id": "46", + "title": "批量 Repo 配置可以整体复制:不要求用户分别进入十几个仓库逐个复制。", + "category": "UI", + "required": true + }, + { + "id": "47", + "title": "原有发布 URL 保持兼容:不得因为新增更友好的系统/版本分类 URL 而破坏已有 /repositories/{publishKey}/... 地址。", + "category": "UI", + "required": true + }, + { + "id": "48", + "title": "仓库节点服务完成 RPM 打包:RPM 至少包含节点程序、systemd unit、默认配置及必要目录创建逻辑。", + "category": "NODE", + "required": true + }, + { + "id": "49", + "title": "RPM 中不得包含节点私钥和长期注册凭据。", + "category": "NODE", + "required": true + }, + { + "id": "50", + "title": "节点页面支持下载安装 RPM:至少能够获得 RPM 下载入口或安装地址。", + "category": "NODE", + "required": true + }, + { + "id": "51", + "title": "节点支持通过 TMS 自动安装 RPM:用户可选择目标机器,通过 TMS 执行下载、安装和服务启动。", + "category": "NODE", + "required": true + }, + { + "id": "52", + "title": "TMS 安装完成后节点自动注册到控制台:用户不再需要手工复制证书指纹、粘贴指纹或人工创建 Node。", + "category": "NODE", + "required": true + }, + { + "id": "53", + "title": "节点注册凭据必须一次性使用:Bootstrap Credential 使用成功后立即失效,不得重复注册。", + "category": "NODE", + "required": true + }, + { + "id": "54", + "title": "节点注册凭据必须短时有效:具有明确 expires_at 或 TTL,不允许使用永久注册 Token。", + "category": "NODE", + "required": true + }, + { + "id": "55", + "title": "节点注册凭据不能形成公共开放注册口:最好与安装任务、TMS Target 或目标节点身份绑定,并具备防 Replay 能力。", + "category": "NODE", + "required": true + }, + { + "id": "56", + "title": "节点私钥只能在节点本地生成:Manager 不得生成、保存或通过 TMS 下发 Node Private Key。", + "category": "NODE", + "required": true + }, + { + "id": "57", + "title": "节点本地私钥权限安全:必须使用安全权限保存,例如仅 root 可读,并不得进入普通任务日志。", + "category": "NODE", + "required": true + }, + { + "id": "58", + "title": "OCAI 分析结果就地展示:点击“OCAI 分析”后不能直接跳聊天页,而是在当前任务或节点 Drawer 内返回分析结果。", + "category": "OCAI", + "required": true + }, + { + "id": "59", + "title": "OCAI 分析提供加载、失败和重新分析状态。", + "category": "OCAI", + "required": true + }, + { + "id": "60", + "title": "保留“在 OCAI 中继续分析”入口:只有用户需要进一步追问时再进入完整聊天页面。", + "category": "OCAI", + "required": true + }, + { + "id": "61", + "title": "用户可见错误信息统一中文化:任务失败原因、处理建议、隔离区拒绝原因、节点预检说明等不得再大段中英文混杂。", + "category": "ERROR", + "required": true + }, + { + "id": "62", + "title": "错误码继续保留英文原码:例如 NODE_INSUFFICIENT_SPACE 不翻译、不删除,便于日志检索。", + "category": "ERROR", + "required": true + }, + { + "id": "63", + "title": "错误体系至少同时包含 Code、中文说明和中文处理建议,不要让每个 Vue 页面自己硬编码英文字符串翻译。", + "category": "ERROR", + "required": true + }, + { + "id": "64", + "title": "软件包支持通过 TMS 批量安装到节点:可在软件包列表选择多个包,再选择多个目标节点执行安装。", + "category": "TMS", + "required": true + }, + { + "id": "65", + "title": "批量安装必须先做 Transaction Preview:绝对不能点击后直接执行 dnf install -y。", + "category": "TMS", + "required": true + }, + { + "id": "66", + "title": "预演结果按机器分别展示:因为不同节点当前安装状态不同。", + "category": "TMS", + "required": true + }, + { + "id": "67", + "title": "安装预演至少展示用户选择包、额外依赖包、将升级的软件包、可能替换的软件包、预计下载量或空间变化。", + "category": "TMS", + "required": true + }, + { + "id": "68", + "title": "用户确认预演结果后才能正式安装。", + "category": "TMS", + "required": true + }, + { + "id": "69", + "title": "批量安装执行结果按节点分别展示:每台节点必须明确成功或失败。", + "category": "TMS", + "required": true + }, + { + "id": "70", + "title": "失败节点优先展示:便于快速排障。", + "category": "TMS", + "required": true + }, + { + "id": "71", + "title": "批量安装失败必须展示错误码、中文原因和必要日志摘要。", + "category": "TMS", + "required": true + }, + { + "id": "72", + "title": "软件包批量安装产生的执行记录必须进入统一任务总览。", + "category": "TMS", + "required": true + }, + { + "id": "73", + "title": "仓库同步支持定时执行:至少支持每天和每周两种常见配置。", + "category": "SYNC", + "required": true + }, + { + "id": "74", + "title": "前端不强迫用户直接填写 Cron:以人可读方式配置时间。", + "category": "SYNC", + "required": true + }, + { + "id": "75", + "title": "定时同步配置完成后显示“下次执行时间”:用户不需要自行计算 Cron。", + "category": "SYNC", + "required": true + }, + { + "id": "76", + "title": "同一仓库上一次同步仍在执行时,不得再次启动新的同步任务。", + "category": "SYNC", + "required": true + }, + { + "id": "77", + "title": "定时同步具备连续失败保护:连续失败达到阈值后自动暂停或禁用该 Schedule,并产生明显告警。", + "category": "SYNC", + "required": true + }, + { + "id": "78", + "title": "不得因上游长期不可用而不断生成大量重复失败任务。", + "category": "SYNC", + "required": true + }, + { + "id": "79", + "title": "任务列表明确区分人工同步和定时同步。", + "category": "SYNC", + "required": true + }, + { + "id": "80", + "title": "现有分片上传升级为真正断点续传:不能只是拥有 chunk API,但断线后仍然从 0 开始。", + "category": "UPLOAD", + "required": true + }, + { + "id": "81", + "title": "服务器保存 Upload Session 状态:至少能识别文件、总大小、Chunk 大小、已上传块和当前状态。", + "category": "UPLOAD", + "required": true + }, + { + "id": "82", + "title": "网络中断后重新选择同一文件能够继续上传缺失分片,不能重新传已完成部分。", + "category": "UPLOAD", + "required": true + }, + { + "id": "83", + "title": "上传支持人工暂停。", + "category": "UPLOAD", + "required": true + }, + { + "id": "84", + "title": "上传支持人工继续。", + "category": "UPLOAD", + "required": true + }, + { + "id": "85", + "title": "上传支持取消操作。", + "category": "UPLOAD", + "required": true + }, + { + "id": "86", + "title": "多文件上传时有队列管理。", + "category": "UPLOAD", + "required": true + }, + { + "id": "87", + "title": "多文件上传展示单文件进度和总体进度。", + "category": "UPLOAD", + "required": true + }, + { + "id": "88", + "title": "超大文件不得强制浏览器先计算完整 SHA256 才允许开始上传;最终服务端仍须完成可靠完整性校验。", + "category": "UPLOAD", + "required": true + }, + { + "id": "89", + "title": "删除软件包之前必须由后端执行依赖影响分析。", + "category": "DELETE", + "required": true + }, + { + "id": "90", + "title": "依赖分析至少识别当前仓库中哪些软件包直接依赖待删包。", + "category": "DELETE", + "required": true + }, + { + "id": "91", + "title": "具备条件时应计算传递依赖影响,并在确认界面中明确展示。", + "category": "DELETE", + "required": true + }, + { + "id": "92", + "title": "有依赖的软件包删除不能再只弹“确认删除吗”。", + "category": "DELETE", + "required": true + }, + { + "id": "93", + "title": "删除确认界面必须展示受影响包、数量和风险。", + "category": "DELETE", + "required": true + }, + { + "id": "94", + "title": "默认安全行为为取消删除,而不是自动级联。", + "category": "DELETE", + "required": true + }, + { + "id": "95", + "title": "如果允许级联删除,必须明确列出将额外删除哪些包,并再次确认。", + "category": "DELETE", + "required": true + }, + { + "id": "96", + "title": "不得静默删除依赖包。", + "category": "DELETE", + "required": true + }, + { + "id": "97", + "title": "前端现有“批量删除生成多个删除任务”的方式必须改掉。", + "category": "DELETE", + "required": true + }, + { + "id": "98", + "title": "批量删除必须实现为真实后端 Batch Delete Task。", + "category": "DELETE", + "required": true + }, + { + "id": "99", + "title": "一次批量删除完成后统一重建仓库元数据,不能删除一个包就执行一次 repodata rebuild。", + "category": "DELETE", + "required": true + }, + { + "id": "100", + "title": "任务总览中一次批量删除表现为一个总任务,任务详情内展示每个包的处理结果。", + "category": "DELETE", + "required": true + }, + { + "id": "101", + "title": "仓库同步支持带宽上限配置。", + "category": "SYNC", + "required": true + }, + { + "id": "102", + "title": "仓库同步支持并发数配置。", + "category": "SYNC", + "required": true + }, + { + "id": "103", + "title": "带宽限制和并发限制必须在后端/Worker 真正生效,不能只在前端保存一个字段。", + "category": "SYNC", + "required": true + }, + { + "id": "104", + "title": "没有设置限制时必须兼容当前同步行为。", + "category": "SYNC", + "required": true + }, + { + "id": "105", + "title": "同步任务详情中能够看到本次使用的限速和并发配置。", + "category": "SYNC", + "required": true + }, + { + "id": "106", + "title": "同步模式明确提供“镜像模式”和“追加模式”。", + "category": "SYNC", + "required": true + }, + { + "id": "107", + "title": "镜像模式语义明确:最终本地仓库与上游保持一致,上游安全删除的软件包本地也进行对应处理。", + "category": "SYNC", + "required": true + }, + { + "id": "108", + "title": "追加模式语义明确:只新增或更新,上游删除的软件包本地继续保留。", + "category": "SYNC", + "required": true + }, + { + "id": "109", + "title": "同步模式在 UI 上写清楚语义,不能让用户自行猜测。", + "category": "SYNC", + "required": true + }, + { + "id": "110", + "title": "老仓库升级后必须有安全默认同步模式,不得因新增字段导致行为突然变化。", + "category": "SYNC", + "required": true + }, + { + "id": "111", + "title": "GPG 策略扩展为三档:关闭、校验但不拦截、严格校验。", + "category": "GPG", + "required": true + }, + { + "id": "112", + "title": "“校验但不拦截”模式下签名问题必须记录并展示 Warning,但允许软件包进入仓库。", + "category": "GPG", + "required": true + }, + { + "id": "113", + "title": "严格校验模式下签名异常必须拒绝或进入隔离区。", + "category": "GPG", + "required": true + }, + { + "id": "114", + "title": "RPM 软件包签名校验和仓库元数据签名必须作为两条独立能力处理。", + "category": "GPG", + "required": true + }, + { + "id": "115", + "title": "不能只把 .repo 中的 repo_gpgcheck 从 0 改成 1 就算完成。", + "category": "GPG", + "required": true + }, + { + "id": "116", + "title": "Repository Metadata 必须真正完成签名链路:至少生成有效的 repomd.xml 和对应签名文件。", + "category": "GPG", + "required": true + }, + { + "id": "117", + "title": "生成的客户端 .repo 配置中的 repo_gpgcheck 必须与服务端实际能力一致。", + "category": "GPG", + "required": true + }, + { + "id": "118", + "title": "GPG Key 配置正确:需要校验时 .repo 中正确给出 gpgkey 等配置。", + "category": "GPG", + "required": true + }, + { + "id": "119", + "title": "GPG 私钥不得下发到浏览器或客户端。", + "category": "GPG", + "required": true + }, + { + "id": "120", + "title": "支持仓库批量导入:不能继续只能导出不能导入。", + "category": "IMPORT", + "required": true + }, + { + "id": "121", + "title": "优先保证“导出格式可以重新导入”,避免维护两套不兼容数据格式。", + "category": "IMPORT", + "required": true + }, + { + "id": "122", + "title": "批量导入必须先解析和校验,不得上传文件后直接创建。", + "category": "IMPORT", + "required": true + }, + { + "id": "123", + "title": "批量导入必须有 Preview:至少展示准备创建、已存在、冲突、无效配置。", + "category": "IMPORT", + "required": true + }, + { + "id": "124", + "title": "用户确认 Preview 后才执行批量创建。", + "category": "IMPORT", + "required": true + }, + { + "id": "125", + "title": "批量导入的重复处理策略明确:至少支持 Skip Existing,若支持 Update Existing 则必须明确提示可能修改哪些内容。", + "category": "IMPORT", + "required": true + }, + { + "id": "126", + "title": "批量导入最好作为统一后端任务进入任务总览,能够查询状态和失败原因。", + "category": "IMPORT", + "required": true + }, + { + "id": "127", + "title": "所有新增功能必须真实连接后端能力:不得存在仅增加按钮、Toast、Mock 数据或循环调用旧 API 来假装实现新能力的情况。", + "category": "INTEGRATION", + "required": true + }, + { + "id": "128", + "title": "数据库新增字段必须通过项目现有 Migration 机制实现:不得要求评审人员手工执行临时 SQL。", + "category": "INTEGRATION", + "required": true + }, + { + "id": "129", + "title": "所有新增字段具备老数据兼容默认值:旧仓库、旧任务、旧节点升级后必须能够正常读取和运行。", + "category": "INTEGRATION", + "required": true + }, + { + "id": "130", + "title": "前端整体视觉风格保持不变:本轮只优化布局、层级、列宽、密度和流程,不重新设计产品视觉。", + "category": "INTEGRATION", + "required": true + }, + { + "id": "131", + "title": "UI 完成后必须实际启动页面验证:至少检查常见 1440px 和 1920px 桌面宽度下的表格、筛选器、Drawer、Dialog、Badge 和滚动行为。", + "category": "INTEGRATION", + "required": true + }, + { + "id": "132", + "title": "不得仅以“前端 build 成功”作为页面验收依据:必须实际查看渲染效果,必要时使用 Playwright、浏览器截图以及 frontend-design / ui-ux-pro-max 类 Skill 做布局复核。", + "category": "INTEGRATION", + "required": true + }, + { + "id": "133", + "title": "后端原有仓库创建、同步、上传、Revision、发布、节点、验证等核心链路不得回归。", + "category": "INTEGRATION", + "required": true + }, + { + "id": "134", + "title": "已有 API 如无必要保持兼容:本轮新增能力优先通过增加字段、增加接口或扩展现有接口实现,避免无必要 Breaking Change。", + "category": "INTEGRATION", + "required": true + }, + { + "id": "135", + "title": "所有任务型功能统一进入任务体系:同步、定时同步、TMS 节点安装、软件包安装、批量删除、批量导入等不能各自形成孤立状态系统。", + "category": "INTEGRATION", + "required": true + }, + { + "id": "136", + "title": "所有任务应具有统一可追踪字段:任务 ID、类型、状态、仓库、节点、触发来源、开始时间、结束时间/耗时、错误码和结果摘要。", + "category": "INTEGRATION", + "required": true + }, + { + "id": "137", + "title": "相关文档同步更新:代码完成但操作文档仍描述旧流程,视为未完成。", + "category": "QUALITY", + "required": true + }, + { + "id": "138", + "title": "文档至少补充新的一级导航、任务总览、节点 RPM/TMS 安装、自动注册安全模型、批量软件安装、定时同步、同步模式、限速、断点续传、依赖删除、GPG 三档策略、Metadata Signing、批量导入和批量 Repo 配置。", + "category": "QUALITY", + "required": true + }, + { + "id": "139", + "title": "测试必须覆盖新增后端逻辑:尤其是一次性注册 Token、同步并发保护、Schedule 连续失败、断点上传恢复、依赖分析、批量删除、GPG 策略和批量导入。", + "category": "QUALITY", + "required": true + }, + { + "id": "140", + "title": "前端至少完成现有 lint、typecheck、build 检查,不能为了赶功能关闭规则或忽略真实错误。", + "category": "QUALITY", + "required": true + }, + { + "id": "141", + "title": "后端至少通过项目原有测试,并为关键新增 Service/API 补测试。", + "category": "QUALITY", + "required": true + }, + { + "id": "142", + "title": "实际环境完成关键端到端验证:不能只依靠单元测试证明 TMS、RPM、DNF、GPG、同步和上传等真实链路可用。", + "category": "E2E", + "required": true + }, + { + "id": "142A", + "title": "节点主视角必须使用真实“单节点多仓库 + 多节点”场景验收:至少构造两个 Repository Node,其中至少一个节点同时承载多个仓库,例如 BaseOS、AppStream、Updates、Private;验证不同节点的仓库列表和容量数据不会串线,每个仓库的逻辑大小和实际占用能够独立统计,并能够从对应节点视角发起同步任务,生成的 Task 必须正确关联 repository_id 和 storage_node_id。", + "category": "E2E", + "required": true + }, + { + "id": "143", + "title": "不得直接修改主干分支上的稳定代码历史:所有本轮修改必须在当前已认可版本基础上的独立开发分支增量实现。", + "category": "GOVERNANCE", + "required": true + }, + { + "id": "144", + "title": "不得为了此次完善做无关大规模重构:如果某项修改与老师反馈、验收或实现必要性无关,不纳入本轮。", + "category": "GOVERNANCE", + "required": true + }, + { + "id": "145", + "title": "最终必须生成一份逐项验收结果:上述每一项标记为“已完成 / 未完成 / 部分完成”,不得用“基本完成”“应该可以”等模糊表述。", + "category": "GOVERNANCE", + "required": true + }, + { + "id": "146", + "title": "对于标记“已完成”的项目必须附验证依据:至少给出对应页面、API、测试、日志、截图、任务记录或代码位置中的一种。", + "category": "GOVERNANCE", + "required": true + }, + { + "id": "147", + "title": "任何未完成项必须明确写出原因和剩余工作,不能隐藏或包装成已完成。", + "category": "GOVERNANCE", + "required": true + }, + { + "id": "148", + "title": "最终目标是确保老师提出的所有修改建议都有明确落点:页面层级、UI 排版、任务聚合、节点运维、TMS、自动化、上传、删除、同步、GPG 和批量管理全部形成真实可操作能力,而不是停留在设计层面。", + "category": "GOVERNANCE", + "required": true + }, + { + "id": "TEACHER-NODE-01", + "title": "首个仓库节点作为受保护的标准 YUM/DNF Node RPM 安装源;前端可选择版本和目标 Agent,由 TMS 添加源、安装并自动注册,注册过程受白名单保护。", + "category": "NODE", + "required": true + } + ] +} diff --git a/manager/acceptance/repository-acceptance-criteria.schema.json b/manager/acceptance/repository-acceptance-criteria.schema.json new file mode 100644 index 0000000000000000000000000000000000000000..2d1dae7ea5dee1b661f1abdccac2006c58735013 --- /dev/null +++ b/manager/acceptance/repository-acceptance-criteria.schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ocmanager.opencloudos.org/schemas/repository-acceptance-criteria.schema.json", + "title": "OCManager Repository Acceptance Criteria", + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "expected_count", "items"], + "properties": { + "schema_version": { "const": 1 }, + "expected_count": { "const": 152 }, + "items": { + "type": "array", + "minItems": 152, + "maxItems": 152, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "title", "category", "required"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "title": { "type": "string", "minLength": 1 }, + "category": { "type": "string", "minLength": 1 }, + "required": { "const": true } + } + } + } + } +} diff --git a/manager/acceptance/repository-acceptance-results.schema.json b/manager/acceptance/repository-acceptance-results.schema.json new file mode 100644 index 0000000000000000000000000000000000000000..0a6d55f600c74f814f0084cc0d0e47704288f146 --- /dev/null +++ b/manager/acceptance/repository-acceptance-results.schema.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ocmanager.opencloudos.org/schemas/repository-acceptance-results.schema.json", + "title": "OCManager Repository Acceptance Results", + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "results"], + "properties": { + "schema_version": { "const": 1 }, + "generated_at": { "type": "string", "format": "date-time" }, + "source_manifest_sha256": { "type": "string", "pattern": "^[0-9a-fA-F]{64}$" }, + "results": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "status", "implementation", "tests", "evidence"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "status": { + "enum": ["NOT_STARTED", "IN_PROGRESS", "BLOCKED", "PASS", "FAIL"] + }, + "implementation": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + }, + "tests": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + }, + "evidence": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + } + } + } + } + } +} diff --git a/manager/acceptance/tests/repository-acceptance-results-test.sh b/manager/acceptance/tests/repository-acceptance-results-test.sh new file mode 100644 index 0000000000000000000000000000000000000000..f905910da347d4c9940f68bfa19cdbee919ca4a5 --- /dev/null +++ b/manager/acceptance/tests/repository-acceptance-results-test.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +set -euo pipefail + +test_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +repo_root=$(cd -- "${test_dir}/../../.." && pwd) +validator=${repo_root}/scripts/repository-acceptance-results.sh +criteria=${repo_root}/manager/acceptance/repository-acceptance-criteria.json +evidence_root=${repo_root}/.run/repository-productization-p9/acceptance/tests +work_dir=$(mktemp -d) + +cleanup() { + rm -rf -- "${work_dir}" +} +trap cleanup EXIT + +expect_failure() { + if "$@" >/dev/null 2>&1; then + printf 'expected command to fail: %q ' "$@" >&2 + printf '\n' >&2 + exit 1 + fi +} + +mkdir -p -- "${evidence_root}" + +bash "${validator}" --criteria "${criteria}" --criteria-only \ + --output-dir "${evidence_root}/criteria-only" + +jq '{schema_version:1,results:[.items[] | { + id:.id,status:"NOT_STARTED",implementation:[],tests:[],evidence:[]}]}' \ + "${criteria}" >"${work_dir}/incomplete.json" +bash "${validator}" --criteria "${criteria}" --results "${work_dir}/incomplete.json" \ + --allow-incomplete --output-dir "${evidence_root}/allow-incomplete" +expect_failure bash "${validator}" --criteria "${criteria}" --results "${work_dir}/incomplete.json" \ + --strict --output-dir "${work_dir}/strict-incomplete" + +jq '{schema_version:1,results:[.items[] | { + id:.id,status:"PASS",implementation:["implementation/" + .id], + tests:["tests/" + .id],evidence:["evidence/" + .id]}]}' \ + "${criteria}" >"${work_dir}/passing.json" +bash "${validator}" --criteria "${criteria}" --results "${work_dir}/passing.json" \ + --strict --output-dir "${evidence_root}/strict-pass" + +jq '.results += [.results[0]]' "${work_dir}/passing.json" >"${work_dir}/duplicate.json" +expect_failure bash "${validator}" --criteria "${criteria}" --results "${work_dir}/duplicate.json" \ + --allow-incomplete --output-dir "${work_dir}/duplicate" + +jq '.results[0].id = "UNEXPECTED-ID"' "${work_dir}/passing.json" >"${work_dir}/unexpected.json" +expect_failure bash "${validator}" --criteria "${criteria}" --results "${work_dir}/unexpected.json" \ + --allow-incomplete --output-dir "${work_dir}/unexpected" + +jq '.results = .results[0:-1]' "${work_dir}/passing.json" >"${work_dir}/missing.json" +expect_failure bash "${validator}" --criteria "${criteria}" --results "${work_dir}/missing.json" \ + --allow-incomplete --output-dir "${work_dir}/missing-allowed" +expect_failure bash "${validator}" --criteria "${criteria}" --results "${work_dir}/missing.json" \ + --strict --output-dir "${work_dir}/missing-strict" + +jq '.results[0].evidence = []' "${work_dir}/passing.json" >"${work_dir}/empty-evidence.json" +expect_failure bash "${validator}" --criteria "${criteria}" --results "${work_dir}/empty-evidence.json" \ + --strict --output-dir "${work_dir}/empty-evidence" + +printf 'repository acceptance results tests: PASS\n' diff --git a/manager/backend/Dockerfile.backend b/manager/backend/Dockerfile.backend index 305999d392e94dc1d3ca4999b9c6d99ab8f9ca95..25a718d742199fb14ce4b8fff67e2d03362462f7 100644 --- a/manager/backend/Dockerfile.backend +++ b/manager/backend/Dockerfile.backend @@ -1,8 +1,8 @@ -# ocmanager 后端运行时镜像(基镜像经 mirror.ccs.tencentyun.com 拉取 Docker Hub 的 library/alpine) +# ocmanager 后端运行时镜像 # # 前置条件:宿主机已执行 make -C manager/backend build-bin-static, # 在 manager/backend/bin/ 下生成静态链接的各服务可执行文件。 -# (必须使用 build-bin-static,CGO_ENABLED=0,以确保在 Alpine 容器中运行) +# (必须使用 build-bin-static,CGO_ENABLED=0,以兼容精简运行时) # (deploy.sh 会自动完成这一步) # # 注意:构建上下文须为 manager/ 目录,docker-compose.yml 已正确配置。 @@ -14,20 +14,41 @@ # manager/deployments/compose/docker-compose.yml ARG SERVICE=footstone -# 基镜像可经 compose 的 RUNTIME_IMAGE 构建参数覆盖(默认腾讯云镜像源) -ARG RUNTIME_IMAGE=mirror.ccs.tencentyun.com/library/alpine:3.19 +# 基镜像可经 compose 的 RUNTIME_IMAGE 构建参数覆盖。 +ARG RUNTIME_IMAGE=docker.io/library/alpine:3.19 FROM ${RUNTIME_IMAGE} -RUN apk add --no-cache \ - gettext \ - tzdata \ - ca-certificates \ - && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ - && echo "Asia/Shanghai" > /etc/timezone \ - && apk del tzdata - ARG SERVICE +ARG CONFIG_SERVICE=${SERVICE} +ARG APK_MIRROR=mirrors.cloud.tencent.com + +RUN set -eu; \ + if command -v apk >/dev/null 2>&1; then \ + if [ "${SERVICE}" = "footstone" ]; then \ + echo "footstone repository dependency preview requires a dnf-capable OpenCloudOS runtime" >&2; \ + exit 1; \ + fi; \ + if [ "${APK_MIRROR}" != "dl-cdn.alpinelinux.org" ]; then \ + sed -i "s#dl-cdn.alpinelinux.org#${APK_MIRROR}#g" /etc/apk/repositories; \ + fi; \ + packages="gettext tzdata ca-certificates"; \ + if [ "${SERVICE}" = "repository-worker" ]; then packages="${packages} rpm"; fi; \ + apk add --no-cache ${packages}; \ + elif command -v dnf >/dev/null 2>&1; then \ + packages="gettext tzdata ca-certificates"; \ + if [ "${SERVICE}" = "footstone" ]; then packages="${packages} curl rpm dnf-plugins-core"; fi; \ + if [ "${SERVICE}" = "repository-worker" ]; then \ + packages="${packages} rpm createrepo_c dnf-plugins-core"; \ + fi; \ + dnf install -y ${packages}; \ + dnf clean all; \ + else \ + echo "unsupported runtime image: apk or dnf is required" >&2; exit 1; \ + fi \ + && ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ + && echo "Asia/Shanghai" > /etc/timezone + ENV SERVICE=${SERVICE} WORKDIR /app @@ -36,12 +57,13 @@ WORKDIR /app COPY backend/bin/${SERVICE} /app/service # 复制配置模板(启动时由 entrypoint 渲染) -COPY backend/services/${SERVICE}/configs/ /app/templates/ +COPY backend/services/${CONFIG_SERVICE}/configs/ /app/templates/ # 复制启动入口 COPY deployments/compose/docker-entrypoint.sh /app/entrypoint.sh -RUN chmod +x /app/entrypoint.sh /app/service \ +RUN sed -i 's/\r$//' /app/entrypoint.sh \ + && chmod +x /app/entrypoint.sh /app/service \ && mkdir -p /app/conf /app/logs EXPOSE 20000 20001 8080 8081 7778 7779 7780 20006 diff --git a/manager/backend/Makefile b/manager/backend/Makefile index b0f49b86e38a92e291ea0821e9394118c338b45b..79df594fcc3019a5bda5093cc7efc27d7a4262bf 100644 --- a/manager/backend/Makefile +++ b/manager/backend/Makefile @@ -19,6 +19,7 @@ build-bin: $(BIN_DIR) $(GO) build -o $(BIN_DIR)/footstone ./cmd/footstone $(GO) build -o $(BIN_DIR)/msg-etl ./cmd/msg-etl $(GO) build -o $(BIN_DIR)/sysdiagnose ./cmd/sysdiagnose + $(GO) build -o $(BIN_DIR)/repository-worker ./cmd/repository-worker # 静态编译(CGO_ENABLED=0),用于 Docker 容器部署 # 消除对 glibc 及内部 SDK 动态库的依赖,生成的二进制可在 Alpine 等镜像中直接运行 @@ -28,6 +29,7 @@ build-bin-static: $(BIN_DIR) CGO_ENABLED=0 GOOS=linux $(GO) build -o $(BIN_DIR)/footstone ./cmd/footstone CGO_ENABLED=0 GOOS=linux $(GO) build -o $(BIN_DIR)/msg-etl ./cmd/msg-etl CGO_ENABLED=0 GOOS=linux $(GO) build -o $(BIN_DIR)/sysdiagnose ./cmd/sysdiagnose + CGO_ENABLED=0 GOOS=linux $(GO) build -o $(BIN_DIR)/repository-worker ./cmd/repository-worker $(BIN_DIR): mkdir -p $(BIN_DIR) diff --git a/manager/backend/cmd/repository-worker/main.go b/manager/backend/cmd/repository-worker/main.go new file mode 100644 index 0000000000000000000000000000000000000000..ae5567bad283e26a431befeef03f93899b56a8ef --- /dev/null +++ b/manager/backend/cmd/repository-worker/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "fmt" + "os" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/repositoryworker/runner" +) + +func main() { + if err := runner.Main(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_10_repository_distribution.sql b/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_10_repository_distribution.sql new file mode 100644 index 0000000000000000000000000000000000000000..f2ce93834f9240134494af9ebb35f9701ba4ccd9 --- /dev/null +++ b/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_10_repository_distribution.sql @@ -0,0 +1,55 @@ +USE `footstone`; + +CREATE TABLE IF NOT EXISTS `repository_distributions` +( + `id` char(36) NOT NULL, + `repository_id` char(36) NOT NULL, + `project_id` bigint NOT NULL, + `revision_id` char(36) NOT NULL, + `repository_task_id` char(36) NOT NULL, + `tms_task_id` varchar(64) NOT NULL DEFAULT '', + `artifact_md5` char(32) NOT NULL DEFAULT '', + `manifest_sha256` char(64) NOT NULL, + `repomd_sha256` char(64) NOT NULL, + `status` varchar(32) NOT NULL, + `requested_by` varchar(256) NOT NULL, + `timeout_seconds` int NOT NULL, + `error_code` varchar(64) NOT NULL DEFAULT '', + `error_message` varchar(2048) NOT NULL DEFAULT '', + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `started_at` datetime(3) NULL, + `finished_at` datetime(3) NULL, + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`), + KEY `idx_repository_distributions_repository` (`repository_id`, `created_at`), + KEY `idx_repository_distributions_revision` (`revision_id`), + KEY `idx_repository_distributions_task` (`repository_task_id`), + KEY `idx_repository_distributions_tms_task` (`tms_task_id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `repository_distribution_targets` +( + `id` char(36) NOT NULL, + `distribution_id` char(36) NOT NULL, + `target_ip` varchar(64) NOT NULL, + `agent_id` bigint unsigned NOT NULL DEFAULT 0, + `status` varchar(32) NOT NULL, + `attempt_count` int NOT NULL DEFAULT 0, + `exit_code` int NOT NULL DEFAULT 0, + `output` mediumtext NOT NULL, + `output_ref` varchar(128) NOT NULL DEFAULT '', + `output_truncated` tinyint(1) NOT NULL DEFAULT 0, + `error_code` varchar(64) NOT NULL DEFAULT '', + `error_message` varchar(2048) NOT NULL DEFAULT '', + `started_at` datetime(3) NULL, + `finished_at` datetime(3) NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_repository_distribution_target` (`distribution_id`, `target_ip`), + KEY `idx_repository_distribution_targets_agent` (`agent_id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci; diff --git a/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_11_repository_gpg_keys.sql b/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_11_repository_gpg_keys.sql new file mode 100644 index 0000000000000000000000000000000000000000..c4826abe2ca625b4bef5453d455cd3bc110f1f8a --- /dev/null +++ b/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_11_repository_gpg_keys.sql @@ -0,0 +1,16 @@ +USE `footstone`; + +CREATE TABLE IF NOT EXISTS `repository_gpg_keys` +( + `id` char(36) NOT NULL, + `repository_id` char(36) NOT NULL, + `key_id` varchar(32) NOT NULL, + `fingerprint` varchar(64) NOT NULL, + `public_key_path` varchar(512) NOT NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_repository_gpg_key_fingerprint` (`repository_id`, `fingerprint`), + KEY `idx_repository_gpg_keys_repository` (`repository_id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci; diff --git a/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_12_repository_storage_nodes.sql b/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_12_repository_storage_nodes.sql new file mode 100644 index 0000000000000000000000000000000000000000..b6e3f1bfa39624823a1fdde30b046c336d546c1d --- /dev/null +++ b/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_12_repository_storage_nodes.sql @@ -0,0 +1,199 @@ +USE `footstone`; + +CREATE TABLE IF NOT EXISTS `repository_storage_nodes` +( + `id` char(36) NOT NULL, + `project_id` bigint NOT NULL, + `name` varchar(128) NOT NULL, + `description` varchar(1024) NOT NULL DEFAULT '', + `control_base_url` varchar(2048) NOT NULL, + `publish_base_url` varchar(2048) NOT NULL, + `tls_server_name` varchar(255) NOT NULL DEFAULT '', + `certificate_fingerprint` char(64) NOT NULL DEFAULT '', + `status` varchar(32) NOT NULL, + `enabled` tinyint(1) NOT NULL DEFAULT 1, + `filesystem_type` varchar(32) NOT NULL DEFAULT '', + `capacity_bytes` bigint NOT NULL DEFAULT 0, + `free_bytes` bigint NOT NULL DEFAULT 0, + `free_inodes` bigint NOT NULL DEFAULT 0, + `tool_versions` json NULL, + `labels` json NULL, + `last_heartbeat_at` datetime(3) NULL, + `last_probe_at` datetime(3) NULL, + `last_error_code` varchar(64) NOT NULL DEFAULT '', + `last_error_message` varchar(2048) NOT NULL DEFAULT '', + `created_by` varchar(256) NOT NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_repository_storage_node_name` (`project_id`, `name`), + KEY `idx_repository_storage_nodes_project` (`project_id`), + KEY `idx_repository_storage_nodes_status` (`status`), + KEY `idx_repository_storage_nodes_heartbeat` (`last_heartbeat_at`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `repository_preflight_records` +( + `id` char(36) NOT NULL, + `storage_node_id` char(36) NOT NULL, + `repository_id` char(36) NOT NULL DEFAULT '', + `task_id` char(36) NOT NULL DEFAULT '', + `status` varchar(32) NOT NULL, + `filesystem_type` varchar(32) NOT NULL DEFAULT '', + `capacity_bytes` bigint NOT NULL DEFAULT 0, + `free_bytes` bigint NOT NULL DEFAULT 0, + `required_free_bytes` bigint NOT NULL DEFAULT 0, + `free_inodes` bigint NOT NULL DEFAULT 0, + `required_inodes` bigint NOT NULL DEFAULT 0, + `estimated_package_count` bigint NOT NULL DEFAULT 0, + `estimated_import_bytes` bigint NOT NULL DEFAULT 0, + `tool_versions` json NULL, + `error_code` varchar(64) NOT NULL DEFAULT '', + `error_message` varchar(2048) NOT NULL DEFAULT '', + `expires_at` datetime(3) NOT NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`), + KEY `idx_repository_preflights_node` (`storage_node_id`, `expires_at`), + KEY `idx_repository_preflights_repository` (`repository_id`), + KEY `idx_repository_preflights_task` (`task_id`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `repository_task_log_indexes` +( + `id` char(36) NOT NULL, + `task_id` char(36) NOT NULL, + `storage_node_id` char(36) NOT NULL, + `relative_path` varchar(512) NOT NULL, + `sha256` char(64) NOT NULL, + `compressed_bytes` bigint NOT NULL, + `uncompressed_bytes` bigint NOT NULL, + `line_count` bigint NOT NULL, + `summary` text NOT NULL, + `error_fingerprint` char(64) NOT NULL DEFAULT '', + `expires_at` datetime(3) NOT NULL, + `deleted_at` datetime(3) NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_repository_task_log_index` (`task_id`), + KEY `idx_repository_task_logs_node` (`storage_node_id`), + KEY `idx_repository_task_logs_fingerprint` (`error_fingerprint`), + KEY `idx_repository_task_logs_expires` (`expires_at`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `repository_ai_analyses` +( + `id` char(36) NOT NULL, + `task_id` char(36) NOT NULL, + `session_id` varchar(128) NOT NULL, + `context_sha256` char(64) NOT NULL, + `status` varchar(32) NOT NULL, + `action_task_id` char(36) NOT NULL DEFAULT '', + `action_result` varchar(32) NOT NULL DEFAULT '', + `requested_by` varchar(256) NOT NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `feedback_at` datetime(3) NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_repository_ai_session` (`session_id`), + KEY `idx_repository_ai_analyses_task` (`task_id`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +DROP PROCEDURE IF EXISTS `ocm_add_column_if_missing`; +DROP PROCEDURE IF EXISTS `ocm_add_index_if_missing`; +DELIMITER // +CREATE PROCEDURE `ocm_add_column_if_missing`( + IN p_table_name varchar(64), + IN p_column_name varchar(64), + IN p_column_definition text +) +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = p_table_name + AND COLUMN_NAME = p_column_name + ) THEN + SET @ocm_ddl = CONCAT( + 'ALTER TABLE `', p_table_name, '` ADD COLUMN `', + p_column_name, '` ', p_column_definition + ); + PREPARE ocm_stmt FROM @ocm_ddl; + EXECUTE ocm_stmt; + DEALLOCATE PREPARE ocm_stmt; + END IF; +END// + +CREATE PROCEDURE `ocm_add_index_if_missing`( + IN p_table_name varchar(64), + IN p_index_name varchar(64), + IN p_index_columns text +) +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = p_table_name + AND INDEX_NAME = p_index_name + ) THEN + SET @ocm_ddl = CONCAT( + 'CREATE INDEX `', p_index_name, '` ON `', + p_table_name, '` (', p_index_columns, ')' + ); + PREPARE ocm_stmt FROM @ocm_ddl; + EXECUTE ocm_stmt; + DEALLOCATE PREPARE ocm_stmt; + END IF; +END// +DELIMITER ; + +CALL `ocm_add_column_if_missing`('software_repositories', 'primary_storage_node_id', + 'char(36) NOT NULL DEFAULT '''' AFTER `active_revision_id`'); +CALL `ocm_add_column_if_missing`('software_repositories', 'publish_base_url_snapshot', + 'varchar(2048) NOT NULL DEFAULT '''' AFTER `primary_storage_node_id`'); +CALL `ocm_add_index_if_missing`('software_repositories', + 'idx_software_repositories_storage_node', '`primary_storage_node_id`'); + +CALL `ocm_add_column_if_missing`('repository_tasks', 'storage_node_id', + 'char(36) NOT NULL DEFAULT '''' AFTER `repository_id`'); +CALL `ocm_add_column_if_missing`('repository_tasks', 'preflight_id', + 'char(36) NOT NULL DEFAULT '''' AFTER `storage_node_id`'); +CALL `ocm_add_column_if_missing`('repository_tasks', 'commit_state', + 'varchar(32) NOT NULL DEFAULT ''NONE'' AFTER `payload`'); +CALL `ocm_add_column_if_missing`('repository_tasks', 'commit_token', + 'char(64) NOT NULL DEFAULT '''' AFTER `commit_state`'); +CALL `ocm_add_column_if_missing`('repository_tasks', 'prepared_revision_id', + 'char(36) NOT NULL DEFAULT '''' AFTER `commit_token`'); +CALL `ocm_add_column_if_missing`('repository_tasks', 'prepared_manifest_sha256', + 'char(64) NOT NULL DEFAULT '''' AFTER `prepared_revision_id`'); +CALL `ocm_add_column_if_missing`('repository_tasks', 'result_object_id', + 'varchar(512) NOT NULL DEFAULT '''' AFTER `prepared_manifest_sha256`'); +CALL `ocm_add_column_if_missing`('repository_tasks', 'result_object_sha256', + 'char(64) NOT NULL DEFAULT '''' AFTER `result_object_id`'); +CALL `ocm_add_column_if_missing`('repository_tasks', 'prepared_at', + 'datetime(3) NULL AFTER `result_object_sha256`'); +CALL `ocm_add_column_if_missing`('repository_tasks', 'activated_at', + 'datetime(3) NULL AFTER `prepared_at`'); +CALL `ocm_add_column_if_missing`('repository_tasks', 'confirmed_at', + 'datetime(3) NULL AFTER `activated_at`'); +CALL `ocm_add_index_if_missing`('repository_tasks', + 'idx_repository_tasks_node_claim', '`storage_node_id`, `status`, `available_at`'); +CALL `ocm_add_index_if_missing`('repository_tasks', + 'idx_repository_tasks_preflight', '`preflight_id`'); + +CALL `ocm_add_column_if_missing`('repository_uploads', 'storage_node_id', + 'char(36) NOT NULL DEFAULT '''' AFTER `project_id`'); +CALL `ocm_add_index_if_missing`('repository_uploads', + 'idx_repository_uploads_storage_node', '`storage_node_id`'); + +CALL `ocm_add_column_if_missing`('repository_revisions', 'storage_node_id', + 'char(36) NOT NULL DEFAULT '''' AFTER `repository_id`'); +CALL `ocm_add_index_if_missing`('repository_revisions', + 'idx_repository_revisions_storage_node', '`storage_node_id`'); + +DROP PROCEDURE `ocm_add_index_if_missing`; +DROP PROCEDURE `ocm_add_column_if_missing`; + +UPDATE `software_repositories` +SET `status` = 'MIGRATION_REQUIRED' +WHERE `primary_storage_node_id` = ''; diff --git a/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_13_repository_task_completion.sql b/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_13_repository_task_completion.sql new file mode 100644 index 0000000000000000000000000000000000000000..ce221135a6c0952a42d581ed2412fab869805b2c --- /dev/null +++ b/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_13_repository_task_completion.sql @@ -0,0 +1,36 @@ +USE `footstone`; + +DROP PROCEDURE IF EXISTS `ocm_add_column_if_missing`; +DELIMITER // +CREATE PROCEDURE `ocm_add_column_if_missing`( + IN p_table_name varchar(64), + IN p_column_name varchar(64), + IN p_column_definition text +) +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = p_table_name + AND COLUMN_NAME = p_column_name + ) THEN + SET @ocm_ddl = CONCAT( + 'ALTER TABLE `', p_table_name, '` ADD COLUMN `', + p_column_name, '` ', p_column_definition + ); + PREPARE ocm_stmt FROM @ocm_ddl; + EXECUTE ocm_stmt; + DEALLOCATE PREPARE ocm_stmt; + END IF; +END// +DELIMITER ; + +CALL `ocm_add_column_if_missing`('repository_tasks', 'prepared_completion_status', + 'varchar(32) NOT NULL DEFAULT ''SUCCESS'' AFTER `result_object_sha256`'); +CALL `ocm_add_column_if_missing`('repository_tasks', 'prepared_error_code', + 'varchar(64) NOT NULL DEFAULT '''' AFTER `prepared_completion_status`'); +CALL `ocm_add_column_if_missing`('repository_tasks', 'prepared_error_message', + 'varchar(2048) NOT NULL DEFAULT '''' AFTER `prepared_error_code`'); + +DROP PROCEDURE `ocm_add_column_if_missing`; diff --git a/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_14_repository_upstream_profiles.sql b/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_14_repository_upstream_profiles.sql new file mode 100644 index 0000000000000000000000000000000000000000..7517d998ce7c1e875af34ea765e1ab7f8bfecd2d --- /dev/null +++ b/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_14_repository_upstream_profiles.sql @@ -0,0 +1,25 @@ +USE `footstone`; + +CREATE TABLE IF NOT EXISTS `repository_upstream_profiles` +( + `id` char(36) NOT NULL, + `project_id` bigint NOT NULL, + `name` varchar(128) NOT NULL, + `description` varchar(1024) NOT NULL DEFAULT '', + `system_product` varchar(128) NOT NULL, + `channel` varchar(64) NOT NULL, + `url_template` varchar(2048) NOT NULL, + `supported_versions` json NOT NULL, + `supported_architectures` json NOT NULL, + `default_include_packages` json NOT NULL, + `default_exclude_packages` json NOT NULL, + `enabled` tinyint(1) NOT NULL DEFAULT 1, + `created_by` varchar(256) NOT NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_repository_upstream_profile_name` (`project_id`, `name`), + KEY `idx_repository_upstream_profiles_project` (`project_id`), + KEY `idx_repository_upstream_profiles_product` (`project_id`, `system_product`), + KEY `idx_repository_upstream_profiles_enabled` (`project_id`, `enabled`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; diff --git a/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_15_repository_productization.sql b/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_15_repository_productization.sql new file mode 100644 index 0000000000000000000000000000000000000000..9e65461508bc465d93a9893e372e8ac4346fd81d --- /dev/null +++ b/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_15_repository_productization.sql @@ -0,0 +1,142 @@ +USE `footstone`; + +DROP PROCEDURE IF EXISTS `ocm_add_column_if_missing`; +DELIMITER // +CREATE PROCEDURE `ocm_add_column_if_missing`( + IN p_table_name varchar(64), + IN p_column_name varchar(64), + IN p_column_definition text +) +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = p_table_name + AND COLUMN_NAME = p_column_name + ) THEN + SET @ocm_ddl = CONCAT('ALTER TABLE `', p_table_name, '` ADD COLUMN `', + p_column_name, '` ', p_column_definition); + PREPARE ocm_stmt FROM @ocm_ddl; + EXECUTE ocm_stmt; + DEALLOCATE PREPARE ocm_stmt; + END IF; +END// +DELIMITER ; + +CALL `ocm_add_column_if_missing`('repository_storage_nodes', 'hostname', + 'varchar(255) NOT NULL DEFAULT '''' AFTER `last_error_message`'); +CALL `ocm_add_column_if_missing`('repository_storage_nodes', 'ip_address', + 'varchar(64) NOT NULL DEFAULT '''' AFTER `hostname`'); +CALL `ocm_add_column_if_missing`('repository_storage_nodes', 'architecture', + 'varchar(32) NOT NULL DEFAULT '''' AFTER `ip_address`'); +CALL `ocm_add_column_if_missing`('repository_storage_nodes', 'os_name', + 'varchar(128) NOT NULL DEFAULT '''' AFTER `architecture`'); +CALL `ocm_add_column_if_missing`('repository_storage_nodes', 'os_version', + 'varchar(64) NOT NULL DEFAULT '''' AFTER `os_name`'); +CALL `ocm_add_column_if_missing`('repository_storage_nodes', 'agent_version', + 'varchar(64) NOT NULL DEFAULT '''' AFTER `os_version`'); + +CALL `ocm_add_column_if_missing`('software_repositories', 'sync_mode', + 'varchar(16) NOT NULL DEFAULT ''MIRROR'' AFTER `publish_base_url_snapshot`'); +CALL `ocm_add_column_if_missing`('software_repositories', 'bandwidth_limit_bytes', + 'bigint NOT NULL DEFAULT 0 AFTER `sync_mode`'); +CALL `ocm_add_column_if_missing`('software_repositories', 'max_parallel_downloads', + 'int NOT NULL DEFAULT 3 AFTER `bandwidth_limit_bytes`'); + +CALL `ocm_add_column_if_missing`('repository_tasks', 'trigger_source', + 'varchar(32) NOT NULL DEFAULT ''MANUAL'' AFTER `task_type`'); +CALL `ocm_add_column_if_missing`('repository_tasks', 'idempotency_key', + 'varchar(128) NULL DEFAULT NULL AFTER `trigger_source`'); +CALL `ocm_add_column_if_missing`('repository_tasks', 'bandwidth_limit_bytes', + 'bigint NOT NULL DEFAULT 0 AFTER `idempotency_key`'); +CALL `ocm_add_column_if_missing`('repository_tasks', 'max_parallel_downloads', + 'int NOT NULL DEFAULT 0 AFTER `bandwidth_limit_bytes`'); + +DROP PROCEDURE `ocm_add_column_if_missing`; + +DROP PROCEDURE IF EXISTS `ocm_add_index_if_missing`; +DELIMITER // +CREATE PROCEDURE `ocm_add_index_if_missing`( + IN p_table_name varchar(64), + IN p_index_name varchar(64), + IN p_index_definition text +) +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = p_table_name + AND INDEX_NAME = p_index_name + ) THEN + SET @ocm_ddl = CONCAT('ALTER TABLE `', p_table_name, '` ADD ', p_index_definition); + PREPARE ocm_stmt FROM @ocm_ddl; + EXECUTE ocm_stmt; + DEALLOCATE PREPARE ocm_stmt; + END IF; +END// +DELIMITER ; + +CALL `ocm_add_index_if_missing`('repository_tasks', 'uniq_repository_task_idempotency', + 'UNIQUE KEY `uniq_repository_task_idempotency` (`idempotency_key`)'); + +DROP PROCEDURE `ocm_add_index_if_missing`; + +CREATE TABLE IF NOT EXISTS `repository_node_repository_usages` +( + `storage_node_id` char(36) NOT NULL, + `repository_id` char(36) NOT NULL, + `logical_bytes` bigint NOT NULL DEFAULT 0, + `physical_bytes` bigint NOT NULL DEFAULT 0, + `publish_status` varchar(32) NOT NULL DEFAULT '', + `observed_at` datetime(3) NOT NULL, + PRIMARY KEY (`storage_node_id`, `repository_id`), + KEY `idx_repository_node_usages_repository` (`repository_id`), + KEY `idx_repository_node_usages_observed` (`observed_at`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `repository_sync_schedules` +( + `id` char(36) NOT NULL, + `repository_id` char(36) NOT NULL, + `project_id` bigint NOT NULL, + `cadence` varchar(16) NOT NULL, + `timezone` varchar(64) NOT NULL DEFAULT 'UTC', + `day_of_week` tinyint NOT NULL DEFAULT 0, + `hour` tinyint NOT NULL DEFAULT 0, + `minute` tinyint NOT NULL DEFAULT 0, + `status` varchar(32) NOT NULL DEFAULT 'ENABLED', + `last_run_at` datetime(3) NULL, + `next_run_at` datetime(3) NOT NULL, + `consecutive_failures` int NOT NULL DEFAULT 0, + `failure_threshold` int NOT NULL DEFAULT 3, + `last_error_code` varchar(64) NOT NULL DEFAULT '', + `last_error_message` varchar(2048) NOT NULL DEFAULT '', + `created_by` varchar(256) NOT NULL, + `created_at` datetime(3) NOT NULL, + `updated_at` datetime(3) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_repository_sync_schedule_repository` (`repository_id`), + KEY `idx_repository_sync_schedules_due` (`status`, `next_run_at`), + KEY `idx_repository_sync_schedules_project` (`project_id`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `repository_schedule_runs` +( + `id` char(36) NOT NULL, + `schedule_id` char(36) NOT NULL, + `repository_id` char(36) NOT NULL, + `scheduled_at` datetime(3) NOT NULL, + `schedule_run_key` char(64) NOT NULL, + `status` varchar(32) NOT NULL, + `task_id` char(36) NOT NULL DEFAULT '', + `error_code` varchar(64) NOT NULL DEFAULT '', + `error_message` varchar(2048) NOT NULL DEFAULT '', + `finished_at` datetime(3) NULL, + `created_at` datetime(3) NOT NULL, + `updated_at` datetime(3) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_repository_schedule_run_key` (`schedule_run_key`), + KEY `idx_repository_schedule_runs_schedule` (`schedule_id`, `scheduled_at`), + KEY `idx_repository_schedule_runs_task` (`task_id`), + KEY `idx_repository_schedule_runs_status` (`status`, `created_at`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; diff --git a/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_16_repository_transfer_and_tms.sql b/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_16_repository_transfer_and_tms.sql new file mode 100644 index 0000000000000000000000000000000000000000..5b958741bd766de40758d3d8c54d82ed4486fad1 --- /dev/null +++ b/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_16_repository_transfer_and_tms.sql @@ -0,0 +1,259 @@ +USE `footstone`; + +-- v16 is intentionally one migration: task targets, node enrollment and +-- resumable transfer share identities that must exist before P4-P6 services run. +DROP PROCEDURE IF EXISTS `ocm_add_column_if_missing`; +DROP PROCEDURE IF EXISTS `ocm_add_index_if_missing`; +DELIMITER // +CREATE PROCEDURE `ocm_add_column_if_missing`( + IN p_table_name varchar(64), + IN p_column_name varchar(64), + IN p_column_definition text +) +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = p_table_name + AND COLUMN_NAME = p_column_name + ) THEN + SET @ocm_ddl = CONCAT('ALTER TABLE `', p_table_name, '` ADD COLUMN `', + p_column_name, '` ', p_column_definition); + PREPARE ocm_stmt FROM @ocm_ddl; + EXECUTE ocm_stmt; + DEALLOCATE PREPARE ocm_stmt; + END IF; +END// + +CREATE PROCEDURE `ocm_add_index_if_missing`( + IN p_table_name varchar(64), + IN p_index_name varchar(64), + IN p_index_definition text +) +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = p_table_name + AND INDEX_NAME = p_index_name + ) THEN + SET @ocm_ddl = CONCAT('ALTER TABLE `', p_table_name, '` ADD ', p_index_definition); + PREPARE ocm_stmt FROM @ocm_ddl; + EXECUTE ocm_stmt; + DEALLOCATE PREPARE ocm_stmt; + END IF; +END// +DELIMITER ; + +CALL `ocm_add_column_if_missing`('repository_tasks', 'scope_type', + 'varchar(32) NOT NULL DEFAULT ''REPOSITORY'' AFTER `project_id`'); +CALL `ocm_add_column_if_missing`('repository_tasks', 'scope_id', + 'varchar(128) NOT NULL DEFAULT '''' AFTER `scope_type`'); +CALL `ocm_add_column_if_missing`('repository_tasks', 'executor_kind', + 'varchar(32) NOT NULL DEFAULT ''REPOSITORY_WORKER'' AFTER `scope_id`'); +CALL `ocm_add_index_if_missing`('repository_tasks', 'idx_repository_tasks_scope', + 'KEY `idx_repository_tasks_scope` (`project_id`, `scope_type`, `scope_id`)'); + +UPDATE `repository_tasks` +SET `scope_id` = `repository_id` +WHERE `scope_type` = 'REPOSITORY' AND `scope_id` = ''; + +CALL `ocm_add_column_if_missing`('repository_uploads', 'client_fingerprint', + 'char(64) NOT NULL DEFAULT '''' AFTER `storage_node_id`'); +CALL `ocm_add_column_if_missing`('repository_uploads', 'chunk_size', + 'int NOT NULL DEFAULT 0 AFTER `expected_size`'); +CALL `ocm_add_column_if_missing`('repository_uploads', 'total_chunks', + 'int NOT NULL DEFAULT 0 AFTER `chunk_size`'); +CALL `ocm_add_column_if_missing`('repository_uploads', 'uploaded_count', + 'int NOT NULL DEFAULT 0 AFTER `total_chunks`'); +CALL `ocm_add_column_if_missing`('repository_uploads', 'paused_at', + 'datetime(3) NULL AFTER `updated_at`'); +CALL `ocm_add_index_if_missing`('repository_uploads', 'idx_repository_uploads_resume', + 'KEY `idx_repository_uploads_resume` (`repository_id`, `requested_by`, `client_fingerprint`, `status`)'); + +CREATE TABLE IF NOT EXISTS `repository_task_targets` +( + `id` char(36) NOT NULL, + `task_id` char(36) NOT NULL, + `target_type` varchar(32) NOT NULL, + `target_id` varchar(128) NOT NULL, + `storage_node_id` char(36) NOT NULL DEFAULT '', + `tms_agent_id` bigint unsigned NOT NULL DEFAULT 0, + `status` varchar(32) NOT NULL, + `progress_percent` int NOT NULL DEFAULT 0, + `started_at` datetime(3) NULL, + `finished_at` datetime(3) NULL, + `error_code` varchar(64) NOT NULL DEFAULT '', + `error_message` varchar(2048) NOT NULL DEFAULT '', + `result_json` json NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_repository_task_target` (`task_id`, `target_type`, `target_id`), + KEY `idx_repository_task_targets_status` (`task_id`, `status`), + KEY `idx_repository_task_targets_node` (`storage_node_id`), + KEY `idx_repository_task_targets_agent` (`tms_agent_id`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `repository_node_enrollments` +( + `id` char(36) NOT NULL, + `node_id` char(36) NOT NULL, + `project_id` bigint NOT NULL, + `tms_agent_id` bigint unsigned NOT NULL DEFAULT 0, + `generation` int NOT NULL, + `status` varchar(32) NOT NULL, + `install_task_id` char(36) NOT NULL, + `tms_task_id` varchar(64) NOT NULL DEFAULT '', + `expected_architecture` varchar(32) NOT NULL, + `expires_at` datetime(3) NOT NULL, + `last_error_code` varchar(64) NOT NULL DEFAULT '', + `last_error_message` varchar(2048) NOT NULL DEFAULT '', + `created_by` varchar(256) NOT NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_repository_node_enrollment_generation` (`node_id`, `generation`), + KEY `idx_repository_node_enrollments_node` (`node_id`), + KEY `idx_repository_node_enrollments_project` (`project_id`), + KEY `idx_repository_node_enrollments_agent` (`tms_agent_id`), + KEY `idx_repository_node_enrollments_status` (`status`), + KEY `idx_repository_node_enrollments_task` (`install_task_id`), + KEY `idx_repository_node_enrollments_tms_task` (`tms_task_id`), + KEY `idx_repository_node_enrollments_expires` (`expires_at`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `repository_node_bootstrap_tokens` +( + `id` char(36) NOT NULL, + `enrollment_id` char(36) NOT NULL, + `generation` int NOT NULL, + `task_id` char(36) NOT NULL, + `tms_agent_id` bigint unsigned NOT NULL DEFAULT 0, + `token_hash` char(64) NOT NULL, + `expires_at` datetime(3) NOT NULL, + `consumed_at` datetime(3) NULL, + `invalidated_at` datetime(3) NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_repository_node_bootstrap_generation` (`enrollment_id`, `generation`), + UNIQUE KEY `uniq_repository_node_bootstrap_hash` (`token_hash`), + KEY `idx_repository_node_bootstrap_task` (`task_id`), + KEY `idx_repository_node_bootstrap_expires` (`expires_at`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `repository_node_certificates` +( + `id` char(36) NOT NULL, + `node_id` char(36) NOT NULL, + `enrollment_id` char(36) NOT NULL DEFAULT '', + `serial_number` varchar(128) NOT NULL, + `fingerprint` char(64) NOT NULL, + `status` varchar(32) NOT NULL, + `not_before` datetime(3) NOT NULL, + `not_after` datetime(3) NOT NULL, + `grace_until` datetime(3) NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_repository_node_certificate_serial` (`serial_number`), + UNIQUE KEY `uniq_repository_node_certificate_fingerprint` (`fingerprint`), + KEY `idx_repository_node_certificates_node` (`node_id`, `status`), + KEY `idx_repository_node_certificates_expiry` (`not_after`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `repository_node_artifacts` +( + `id` char(36) NOT NULL, + `name` varchar(128) NOT NULL, + `version` varchar(64) NOT NULL, + `release` varchar(32) NOT NULL, + `architecture` varchar(32) NOT NULL, + `file_name` varchar(255) NOT NULL, + `size_bytes` bigint NOT NULL, + `sha256` char(64) NOT NULL, + `build_commit` varchar(64) NOT NULL, + `relative_path` varchar(512) NOT NULL, + `status` varchar(32) NOT NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_repository_node_artifact` (`version`, `release`, `architecture`), + KEY `idx_repository_node_artifacts_status` (`status`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `repository_tms_capabilities` +( + `tms_agent_id` bigint unsigned NOT NULL, + `architecture` varchar(32) NOT NULL DEFAULT '', + `os_name` varchar(128) NOT NULL DEFAULT '', + `os_version` varchar(64) NOT NULL DEFAULT '', + `connected` tinyint(1) NOT NULL DEFAULT 0, + `last_heartbeat_at` datetime(3) NULL, + `observed_at` datetime(3) NOT NULL, + PRIMARY KEY (`tms_agent_id`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `repository_upload_chunks` +( + `upload_id` char(36) NOT NULL, + `chunk_index` int NOT NULL, + `size_bytes` int NOT NULL, + `sha256` char(64) NOT NULL, + `stored_path` varchar(512) NOT NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + PRIMARY KEY (`upload_id`, `chunk_index`), + KEY `idx_repository_upload_chunks_created` (`created_at`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `repository_operation_previews` +( + `id` char(36) NOT NULL, + `project_id` bigint NOT NULL, + `operation_type` varchar(32) NOT NULL, + `resource_id` varchar(128) NOT NULL, + `resource_version` varchar(512) NOT NULL, + `preview_digest` char(64) NOT NULL, + `payload` json NOT NULL, + `status` varchar(32) NOT NULL, + `expires_at` datetime(3) NOT NULL, + `created_by` varchar(256) NOT NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `confirmed_at` datetime(3) NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_repository_operation_preview_digest` (`preview_digest`), + KEY `idx_repository_operation_previews_resource` (`project_id`, `operation_type`, `resource_id`), + KEY `idx_repository_operation_previews_expiry` (`status`, `expires_at`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `repository_package_installs` +( + `id` char(36) NOT NULL, + `project_id` bigint NOT NULL, + `repository_id` char(36) NOT NULL, + `revision_id` char(36) NOT NULL, + `preview_id` char(36) NOT NULL, + `repository_task_id` char(36) NOT NULL, + `tms_task_id` varchar(64) NOT NULL DEFAULT '', + `artifact_md5` char(32) NOT NULL DEFAULT '', + `status` varchar(32) NOT NULL, + `timeout_seconds` int NOT NULL, + `requested_by` varchar(256) NOT NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_repository_package_install_preview` (`preview_id`), + UNIQUE KEY `uniq_repository_package_install_task` (`repository_task_id`), + KEY `idx_repository_package_installs_project` (`project_id`), + KEY `idx_repository_package_installs_repository` (`repository_id`), + KEY `idx_repository_package_installs_status` (`status`), + KEY `idx_repository_package_installs_tms_task` (`tms_task_id`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +-- Keep v16 re-runnable for databases that applied an earlier P4 prototype +-- before TMS task reconciliation was added to node enrollment. +CALL `ocm_add_column_if_missing`('repository_node_enrollments', 'tms_task_id', + 'varchar(64) NOT NULL DEFAULT '''' AFTER `install_task_id`'); +CALL `ocm_add_index_if_missing`('repository_node_enrollments', 'idx_repository_node_enrollments_tms_task', + 'KEY `idx_repository_node_enrollments_tms_task` (`tms_task_id`)'); + +DROP PROCEDURE `ocm_add_index_if_missing`; +DROP PROCEDURE `ocm_add_column_if_missing`; diff --git a/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_17_repository_gpg_import.sql b/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_17_repository_gpg_import.sql new file mode 100644 index 0000000000000000000000000000000000000000..06fdd35dfc52a9912b8eb582e72463acb489819a --- /dev/null +++ b/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_17_repository_gpg_import.sql @@ -0,0 +1,104 @@ +USE `footstone`; + +-- P7 keeps package audit, repository metadata signing and inventory import in +-- one migration because they share repository definition and revision identity. +DROP PROCEDURE IF EXISTS `ocm_add_column_if_missing`; +DROP PROCEDURE IF EXISTS `ocm_add_index_if_missing`; +DELIMITER // +CREATE PROCEDURE `ocm_add_column_if_missing`( + IN p_table_name varchar(64), IN p_column_name varchar(64), IN p_column_definition text +) +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = p_table_name AND COLUMN_NAME = p_column_name + ) THEN + SET @ocm_ddl = CONCAT('ALTER TABLE `', p_table_name, '` ADD COLUMN `', + p_column_name, '` ', p_column_definition); + PREPARE ocm_stmt FROM @ocm_ddl; + EXECUTE ocm_stmt; + DEALLOCATE PREPARE ocm_stmt; + END IF; +END// + +CREATE PROCEDURE `ocm_add_index_if_missing`( + IN p_table_name varchar(64), IN p_index_name varchar(64), IN p_index_definition text +) +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = p_table_name AND INDEX_NAME = p_index_name + ) THEN + SET @ocm_ddl = CONCAT('ALTER TABLE `', p_table_name, '` ADD ', p_index_definition); + PREPARE ocm_stmt FROM @ocm_ddl; + EXECUTE ocm_stmt; + DEALLOCATE PREPARE ocm_stmt; + END IF; +END// +DELIMITER ; + +CALL `ocm_add_column_if_missing`('repository_packages', 'signature_status', + 'varchar(32) NOT NULL DEFAULT ''UNCHECKED'' AFTER `source_upload_id`'); +CALL `ocm_add_column_if_missing`('repository_packages', 'signer_key_id', + 'varchar(64) NOT NULL DEFAULT '''' AFTER `signature_status`'); +CALL `ocm_add_column_if_missing`('repository_packages', 'signature_message', + 'varchar(1024) NOT NULL DEFAULT '''' AFTER `signer_key_id`'); +CALL `ocm_add_index_if_missing`('repository_packages', 'idx_repository_packages_signature', + 'KEY `idx_repository_packages_signature` (`signature_status`)'); + +CALL `ocm_add_column_if_missing`('software_repositories', 'metadata_signing_key_ref', + 'varchar(128) NOT NULL DEFAULT '''' AFTER `max_parallel_downloads`'); + +CALL `ocm_add_column_if_missing`('repository_revisions', 'metadata_signed', + 'tinyint(1) NOT NULL DEFAULT 0 AFTER `repomd_sha256`'); +CALL `ocm_add_column_if_missing`('repository_revisions', 'metadata_signing_key_ref', + 'varchar(128) NOT NULL DEFAULT '''' AFTER `metadata_signed`'); +CALL `ocm_add_column_if_missing`('repository_revisions', 'metadata_signing_fingerprint', + 'varchar(64) NOT NULL DEFAULT '''' AFTER `metadata_signing_key_ref`'); +CALL `ocm_add_column_if_missing`('repository_revisions', 'repomd_signature_path', + 'varchar(512) NOT NULL DEFAULT '''' AFTER `metadata_signing_fingerprint`'); +CALL `ocm_add_column_if_missing`('repository_revisions', 'metadata_public_key_url', + 'varchar(2048) NOT NULL DEFAULT '''' AFTER `repomd_signature_path`'); + +CREATE TABLE IF NOT EXISTS `repository_metadata_signing_keys` +( + `id` char(36) NOT NULL, + `storage_node_id` char(36) NOT NULL, + `key_ref` varchar(128) NOT NULL, + `fingerprint` varchar(64) NOT NULL, + `public_key_name` varchar(255) NOT NULL, + `public_key_url` varchar(2048) NOT NULL DEFAULT '', + `status` varchar(32) NOT NULL, + `key_created_at` datetime(3) NULL, + `observed_at` datetime(3) NOT NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_repository_metadata_key_ref` (`storage_node_id`, `key_ref`), + KEY `idx_repository_metadata_keys_status` (`storage_node_id`, `status`), + KEY `idx_repository_metadata_keys_fingerprint` (`fingerprint`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `repository_inventory_import_jobs` +( + `id` char(36) NOT NULL, + `project_id` bigint NOT NULL, + `preview_id` char(36) NOT NULL, + `schema_version` varchar(64) NOT NULL, + `conflict_strategy` varchar(32) NOT NULL, + `status` varchar(32) NOT NULL, + `mapping_json` json NOT NULL, + `summary_json` json NOT NULL, + `error_code` varchar(64) NOT NULL DEFAULT '', + `error_message` varchar(2048) NOT NULL DEFAULT '', + `requested_by` varchar(256) NOT NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `finished_at` datetime(3) NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_repository_inventory_import_preview` (`preview_id`), + KEY `idx_repository_inventory_import_project` (`project_id`, `created_at`), + KEY `idx_repository_inventory_import_status` (`status`, `created_at`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +DROP PROCEDURE `ocm_add_index_if_missing`; +DROP PROCEDURE `ocm_add_column_if_missing`; diff --git a/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_18_repository_acceptance_closure.sql b/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_18_repository_acceptance_closure.sql new file mode 100644 index 0000000000000000000000000000000000000000..031d6908c3f8570db173ead79059316fa9f88e5d --- /dev/null +++ b/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_18_repository_acceptance_closure.sql @@ -0,0 +1,236 @@ +USE `footstone`; + +-- Third-stage acceptance schema. This migration is intentionally complete and +-- must not be edited after it has been applied to any environment. +DROP PROCEDURE IF EXISTS `ocm_add_column_if_missing`; +DROP PROCEDURE IF EXISTS `ocm_add_index_if_missing`; +DROP PROCEDURE IF EXISTS `ocm_drop_index_if_exists`; +DROP PROCEDURE IF EXISTS `ocm_add_fk_if_missing`; +DELIMITER // +CREATE PROCEDURE `ocm_add_column_if_missing`( + IN p_table_name varchar(64), IN p_column_name varchar(64), IN p_column_definition text +) +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = p_table_name + AND COLUMN_NAME = p_column_name + ) THEN + SET @ocm_ddl = CONCAT('ALTER TABLE `', p_table_name, '` ADD COLUMN `', + p_column_name, '` ', p_column_definition); + PREPARE ocm_stmt FROM @ocm_ddl; + EXECUTE ocm_stmt; + DEALLOCATE PREPARE ocm_stmt; + END IF; +END// + +CREATE PROCEDURE `ocm_add_index_if_missing`( + IN p_table_name varchar(64), IN p_index_name varchar(64), IN p_index_definition text +) +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = p_table_name + AND INDEX_NAME = p_index_name + ) THEN + SET @ocm_ddl = CONCAT('ALTER TABLE `', p_table_name, '` ADD ', p_index_definition); + PREPARE ocm_stmt FROM @ocm_ddl; + EXECUTE ocm_stmt; + DEALLOCATE PREPARE ocm_stmt; + END IF; +END// + +CREATE PROCEDURE `ocm_drop_index_if_exists`( + IN p_table_name varchar(64), IN p_index_name varchar(64) +) +BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = p_table_name + AND INDEX_NAME = p_index_name + ) THEN + SET @ocm_ddl = CONCAT('ALTER TABLE `', p_table_name, '` DROP INDEX `', p_index_name, '`'); + PREPARE ocm_stmt FROM @ocm_ddl; + EXECUTE ocm_stmt; + DEALLOCATE PREPARE ocm_stmt; + END IF; +END// + +CREATE PROCEDURE `ocm_add_fk_if_missing`( + IN p_table_name varchar(64), IN p_constraint_name varchar(64), IN p_constraint_definition text +) +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.REFERENTIAL_CONSTRAINTS + WHERE CONSTRAINT_SCHEMA = DATABASE() + AND TABLE_NAME = p_table_name + AND CONSTRAINT_NAME = p_constraint_name + ) THEN + SET @ocm_ddl = CONCAT('ALTER TABLE `', p_table_name, '` ADD CONSTRAINT `', + p_constraint_name, '` ', p_constraint_definition); + PREPARE ocm_stmt FROM @ocm_ddl; + EXECUTE ocm_stmt; + DEALLOCATE PREPARE ocm_stmt; + END IF; +END// +DELIMITER ; + +-- A Node RPM source remains an ordinary private Repository. These fields only +-- protect its system purpose and do not introduce a second repository type. +CALL `ocm_add_column_if_missing`('software_repositories', 'system_managed', + 'tinyint(1) NOT NULL DEFAULT 0 AFTER `metadata_signing_key_ref`'); +CALL `ocm_add_column_if_missing`('software_repositories', 'purpose', + 'varchar(32) NOT NULL DEFAULT '''' AFTER `system_managed`'); +CALL `ocm_add_index_if_missing`('software_repositories', 'idx_software_repositories_purpose', + 'KEY `idx_software_repositories_purpose` (`project_id`, `purpose`, `architecture`, `enabled`)'); + +CREATE TABLE IF NOT EXISTS `repository_node_install_sources` +( + `id` char(36) NOT NULL, + `project_id` bigint NOT NULL, + `repository_id` char(36) NOT NULL, + `architecture` varchar(32) NOT NULL, + `status` varchar(32) NOT NULL DEFAULT 'ENABLED', + `created_by` varchar(256) NOT NULL, + `updated_by` varchar(256) NOT NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_repository_node_install_source_arch` (`project_id`, `architecture`), + KEY `idx_repository_node_install_sources_repository` (`repository_id`), + CONSTRAINT `fk_repository_node_install_source_repository` + FOREIGN KEY (`repository_id`) REFERENCES `software_repositories` (`id`) ON DELETE RESTRICT +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `repository_node_registration_allowlists` +( + `id` char(36) NOT NULL, + `project_id` bigint NOT NULL, + `tms_agent_id` bigint unsigned NOT NULL, + `expected_hostname` varchar(255) NOT NULL DEFAULT '', + `expected_ip` varchar(64) NOT NULL DEFAULT '', + `expected_architecture` varchar(32) NOT NULL DEFAULT '', + `status` varchar(32) NOT NULL DEFAULT 'ENABLED', + `expires_at` datetime(3) NULL, + `created_by` varchar(256) NOT NULL, + `updated_by` varchar(256) NOT NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_repository_node_allowlist_agent` (`project_id`, `tms_agent_id`), + KEY `idx_repository_node_allowlists_status` (`project_id`, `status`, `expires_at`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +-- Nullable references keep all v16 enrollment rows valid during upgrade. New +-- P12 enrollments are required by the service layer to populate both links. +CALL `ocm_add_column_if_missing`('repository_node_enrollments', 'allowlist_entry_id', + 'char(36) NULL DEFAULT NULL AFTER `tms_task_id`'); +CALL `ocm_add_column_if_missing`('repository_node_enrollments', 'artifact_id', + 'char(36) NULL DEFAULT NULL AFTER `allowlist_entry_id`'); +CALL `ocm_add_column_if_missing`('repository_node_enrollments', 'authorization_id', + 'char(36) NULL DEFAULT NULL AFTER `artifact_id`'); +CALL `ocm_add_column_if_missing`('repository_node_enrollments', 'csr_sha256', + 'char(64) NOT NULL DEFAULT '''' AFTER `authorization_id`'); +CALL `ocm_add_column_if_missing`('repository_node_enrollments', 'authorized_at', + 'datetime(3) NULL AFTER `csr_sha256`'); +CALL `ocm_add_column_if_missing`('repository_node_enrollments', 'issuance_status', + 'varchar(32) NOT NULL DEFAULT ''PENDING'' AFTER `authorized_at`'); +CALL `ocm_add_column_if_missing`('repository_node_enrollments', 'issuance_lease_owner', + 'varchar(128) NOT NULL DEFAULT '''' AFTER `issuance_status`'); +CALL `ocm_add_column_if_missing`('repository_node_enrollments', 'issuance_lease_expires_at', + 'datetime(3) NULL AFTER `issuance_lease_owner`'); +CALL `ocm_add_index_if_missing`('repository_node_enrollments', 'uniq_repository_node_enrollment_authorization', + 'UNIQUE KEY `uniq_repository_node_enrollment_authorization` (`authorization_id`)'); +CALL `ocm_add_index_if_missing`('repository_node_enrollments', 'idx_repository_node_enrollments_allowlist', + 'KEY `idx_repository_node_enrollments_allowlist` (`allowlist_entry_id`)'); +CALL `ocm_add_index_if_missing`('repository_node_enrollments', 'idx_repository_node_enrollments_artifact', + 'KEY `idx_repository_node_enrollments_artifact` (`artifact_id`)'); +CALL `ocm_add_index_if_missing`('repository_node_enrollments', 'idx_repository_node_enrollments_issuance', + 'KEY `idx_repository_node_enrollments_issuance` (`issuance_status`, `issuance_lease_expires_at`)'); + +-- Preserve the existing repository_node_certificates table as the sole public +-- certificate source of truth. No private key or CA private key is stored. +CALL `ocm_add_column_if_missing`('repository_node_certificates', 'authorization_id', + 'char(36) NULL DEFAULT NULL AFTER `enrollment_id`'); +CALL `ocm_add_column_if_missing`('repository_node_certificates', 'certificate_pem', + 'mediumtext NULL AFTER `fingerprint`'); +CALL `ocm_add_column_if_missing`('repository_node_certificates', 'certificate_chain_pem', + 'mediumtext NULL AFTER `certificate_pem`'); +CALL `ocm_add_column_if_missing`('repository_node_certificates', 'issuer_fingerprint', + 'char(64) NOT NULL DEFAULT '''' AFTER `certificate_chain_pem`'); +CALL `ocm_add_index_if_missing`('repository_node_certificates', 'uniq_repository_node_certificate_authorization', + 'UNIQUE KEY `uniq_repository_node_certificate_authorization` (`authorization_id`)'); + +-- Existing v16 artifacts are explicitly legacy local files. Repository-backed +-- artifacts must populate project/source/revision/package/NEVRA/immutable URL. +CALL `ocm_add_column_if_missing`('repository_node_artifacts', 'project_id', + 'bigint NOT NULL DEFAULT 0 AFTER `id`'); +CALL `ocm_add_column_if_missing`('repository_node_artifacts', 'source_kind', + 'varchar(32) NOT NULL DEFAULT ''LEGACY_LOCAL'' AFTER `project_id`'); +CALL `ocm_add_column_if_missing`('repository_node_artifacts', 'source_repository_id', + 'char(36) NULL DEFAULT NULL AFTER `source_kind`'); +CALL `ocm_add_column_if_missing`('repository_node_artifacts', 'revision_id', + 'char(36) NULL DEFAULT NULL AFTER `source_repository_id`'); +CALL `ocm_add_column_if_missing`('repository_node_artifacts', 'package_id', + 'char(36) NULL DEFAULT NULL AFTER `revision_id`'); +CALL `ocm_add_column_if_missing`('repository_node_artifacts', 'epoch', + 'bigint unsigned NOT NULL DEFAULT 0 AFTER `name`'); +CALL `ocm_add_column_if_missing`('repository_node_artifacts', 'nevra', + 'varchar(512) NOT NULL DEFAULT '''' AFTER `architecture`'); +CALL `ocm_add_column_if_missing`('repository_node_artifacts', 'immutable_url', + 'varchar(2048) NOT NULL DEFAULT '''' AFTER `relative_path`'); +CALL `ocm_drop_index_if_exists`('repository_node_artifacts', 'uniq_repository_node_artifact'); +CALL `ocm_add_index_if_missing`('repository_node_artifacts', 'uniq_repository_node_artifact_evr_source', + 'UNIQUE KEY `uniq_repository_node_artifact_evr_source` (`project_id`, `source_repository_id`, `name`, `epoch`, `version`, `release`, `architecture`)'); +CALL `ocm_add_index_if_missing`('repository_node_artifacts', 'idx_repository_node_artifacts_selection', + 'KEY `idx_repository_node_artifacts_selection` (`project_id`, `architecture`, `source_kind`, `status`)'); +CALL `ocm_add_index_if_missing`('repository_node_artifacts', 'idx_repository_node_artifacts_revision', + 'KEY `idx_repository_node_artifacts_revision` (`revision_id`, `package_id`)'); + +CREATE TABLE IF NOT EXISTS `repository_task_result_items` +( + `id` char(36) NOT NULL, + `task_id` char(36) NOT NULL, + `item_key` varchar(512) NOT NULL, + `package_nevra` varchar(512) NOT NULL DEFAULT '', + `status` varchar(32) NOT NULL, + `error_code` varchar(64) NOT NULL DEFAULT '', + `message` varchar(2048) NOT NULL DEFAULT '', + `result_json` json NULL, + `started_at` datetime(3) NULL, + `finished_at` datetime(3) NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_repository_task_result_item` (`task_id`, `item_key`), + KEY `idx_repository_task_result_items_status` (`task_id`, `status`), + CONSTRAINT `fk_repository_task_result_item_task` + FOREIGN KEY (`task_id`) REFERENCES `repository_tasks` (`id`) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +CALL `ocm_add_column_if_missing`('repository_inventory_import_jobs', 'repository_task_id', + 'char(36) NULL DEFAULT NULL AFTER `preview_id`'); +CALL `ocm_add_index_if_missing`('repository_inventory_import_jobs', 'uniq_repository_inventory_import_task', + 'UNIQUE KEY `uniq_repository_inventory_import_task` (`repository_task_id`)'); + +-- Add nullable compatibility foreign keys only after columns and indexes exist. +-- Existing rows have NULL references and therefore remain valid. +CALL `ocm_add_fk_if_missing`('repository_node_enrollments', 'fk_repository_node_enrollment_allowlist', + 'FOREIGN KEY (`allowlist_entry_id`) REFERENCES `repository_node_registration_allowlists` (`id`) ON DELETE RESTRICT'); +CALL `ocm_add_fk_if_missing`('repository_node_enrollments', 'fk_repository_node_enrollment_artifact', + 'FOREIGN KEY (`artifact_id`) REFERENCES `repository_node_artifacts` (`id`) ON DELETE RESTRICT'); +CALL `ocm_add_fk_if_missing`('repository_node_artifacts', 'fk_repository_node_artifact_repository', + 'FOREIGN KEY (`source_repository_id`) REFERENCES `software_repositories` (`id`) ON DELETE RESTRICT'); +CALL `ocm_add_fk_if_missing`('repository_node_artifacts', 'fk_repository_node_artifact_revision', + 'FOREIGN KEY (`revision_id`) REFERENCES `repository_revisions` (`id`) ON DELETE RESTRICT'); +CALL `ocm_add_fk_if_missing`('repository_node_artifacts', 'fk_repository_node_artifact_package', + 'FOREIGN KEY (`package_id`) REFERENCES `repository_packages` (`id`) ON DELETE RESTRICT'); +CALL `ocm_add_fk_if_missing`('repository_inventory_import_jobs', 'fk_repository_inventory_import_task', + 'FOREIGN KEY (`repository_task_id`) REFERENCES `repository_tasks` (`id`) ON DELETE RESTRICT'); + +DROP PROCEDURE `ocm_add_fk_if_missing`; +DROP PROCEDURE `ocm_drop_index_if_exists`; +DROP PROCEDURE `ocm_add_index_if_missing`; +DROP PROCEDURE `ocm_add_column_if_missing`; diff --git a/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_5_software_repositories.sql b/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_5_software_repositories.sql new file mode 100644 index 0000000000000000000000000000000000000000..ebd6bfe88205cfba0b02538a76de009a0ea92c92 --- /dev/null +++ b/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_5_software_repositories.sql @@ -0,0 +1,35 @@ +USE `footstone`; + +CREATE TABLE IF NOT EXISTS `software_repositories` +( + `id` char(36) NOT NULL COMMENT 'Repository UUID', + `project_id` bigint NOT NULL COMMENT 'Owning project ID', + `system_product` varchar(128) NOT NULL COMMENT 'System or product name', + `product_version` varchar(64) NOT NULL COMMENT 'Product version', + `name` varchar(128) NOT NULL COMMENT 'Repository name', + `description` varchar(1024) NOT NULL DEFAULT '' COMMENT 'Repository description', + `architecture` varchar(32) NOT NULL COMMENT 'x86_64, aarch64, or noarch', + `repository_type` varchar(32) NOT NULL COMMENT 'BASELINE, UPDATE, or PRIVATE', + `upstream_url` varchar(2048) NOT NULL DEFAULT '' COMMENT 'Optional upstream repository URL', + `storage_location` varchar(512) NOT NULL COMMENT 'Server-generated path relative to repository data root', + `publish_key` varchar(64) NOT NULL COMMENT 'Stable HTTP publication key', + `allow_upload` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Whether management upload is enabled', + `include_packages` json NOT NULL COMMENT 'Remote sync include patterns', + `exclude_packages` json NOT NULL COMMENT 'Remote sync exclude patterns', + `gpg_policy` varchar(32) NOT NULL COMMENT 'DISABLED or VERIFY_PACKAGE', + `enabled` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'Whether repository operations are enabled', + `status` varchar(32) NOT NULL COMMENT 'Repository lifecycle status', + `active_revision_id` char(36) NOT NULL DEFAULT '' COMMENT 'Current immutable revision ID', + `creator` varchar(256) NOT NULL COMMENT 'Login name of creator', + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_software_repository_scope` + (`project_id`, `system_product`, `product_version`, `name`, `architecture`), + UNIQUE KEY `idx_software_repositories_publish_key` (`publish_key`), + KEY `idx_software_repositories_project` (`project_id`), + KEY `idx_software_repositories_group` (`project_id`, `system_product`, `product_version`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci + COMMENT = 'OCManager software repository definitions'; diff --git a/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_6_repository_tasks.sql b/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_6_repository_tasks.sql new file mode 100644 index 0000000000000000000000000000000000000000..8eef29c7023c820695475a363a0591106ba8c42a --- /dev/null +++ b/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_6_repository_tasks.sql @@ -0,0 +1,58 @@ +USE `footstone`; + +CREATE TABLE IF NOT EXISTS `repository_tasks` +( + `id` char(36) NOT NULL, + `root_task_id` char(36) NOT NULL, + `retry_of_task_id` char(36) NOT NULL DEFAULT '', + `repository_id` char(36) NOT NULL, + `project_id` bigint NOT NULL, + `revision_id` char(36) NOT NULL DEFAULT '', + `task_type` varchar(32) NOT NULL, + `status` varchar(32) NOT NULL, + `requires_mutation_lock` tinyint(1) NOT NULL, + `attempt` int NOT NULL DEFAULT 0, + `max_attempts` int NOT NULL, + `requested_by` varchar(256) NOT NULL, + `worker_id` varchar(128) NOT NULL DEFAULT '', + `fencing_token` bigint unsigned NOT NULL DEFAULT 0, + `lease_expires_at` datetime(3) NULL, + `last_heartbeat_at` datetime(3) NULL, + `available_at` datetime(3) NOT NULL, + `started_at` datetime(3) NULL, + `finished_at` datetime(3) NULL, + `cancel_requested_at` datetime(3) NULL, + `progress_percent` int NOT NULL DEFAULT 0, + `current_step` varchar(128) NOT NULL DEFAULT '', + `error_code` varchar(64) NOT NULL DEFAULT '', + `error_message` varchar(2048) NOT NULL DEFAULT '', + `payload` json NOT NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`), + KEY `idx_repository_tasks_root` (`root_task_id`), + KEY `idx_repository_tasks_repository` (`repository_id`, `created_at`), + KEY `idx_repository_tasks_project` (`project_id`), + KEY `idx_repository_tasks_claim` (`status`, `available_at`, `created_at`), + KEY `idx_repository_tasks_lease` (`lease_expires_at`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `repository_mutation_locks` +( + `repository_id` char(36) NOT NULL, + `owner_task_id` char(36) NOT NULL DEFAULT '', + `worker_id` varchar(128) NOT NULL DEFAULT '', + `fencing_token` bigint unsigned NOT NULL DEFAULT 0, + `lease_expires_at` datetime(3) NULL, + `last_heartbeat_at` datetime(3) NULL, + `acquired_at` datetime(3) NULL, + `released_at` datetime(3) NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (`repository_id`), + KEY `idx_repository_mutation_locks_lease` (`lease_expires_at`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci; diff --git a/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_7_repository_content.sql b/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_7_repository_content.sql new file mode 100644 index 0000000000000000000000000000000000000000..d13e66cc757ff6028ba1b9ebf5186ccbd5b5b63c --- /dev/null +++ b/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_7_repository_content.sql @@ -0,0 +1,98 @@ +USE `footstone`; + +CREATE TABLE IF NOT EXISTS `repository_uploads` +( + `id` char(36) NOT NULL, + `repository_id` char(36) NOT NULL, + `project_id` bigint NOT NULL, + `task_id` char(36) NOT NULL DEFAULT '', + `file_name` varchar(255) NOT NULL, + `upload_kind` varchar(16) NOT NULL, + `expected_size` bigint NOT NULL, + `received_size` bigint NOT NULL DEFAULT 0, + `expected_sha256` char(64) NOT NULL DEFAULT '', + `actual_sha256` char(64) NOT NULL DEFAULT '', + `relative_path` varchar(512) NOT NULL, + `status` varchar(32) NOT NULL, + `requested_by` varchar(256) NOT NULL, + `error_code` varchar(64) NOT NULL DEFAULT '', + `error_message` varchar(2048) NOT NULL DEFAULT '', + `expires_at` datetime(3) NOT NULL, + `completed_at` datetime(3) NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`), + KEY `idx_repository_uploads_repository` (`repository_id`), + KEY `idx_repository_uploads_project` (`project_id`), + KEY `idx_repository_uploads_task` (`task_id`), + KEY `idx_repository_uploads_status` (`status`), + KEY `idx_repository_uploads_expires` (`expires_at`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `repository_revisions` +( + `id` char(36) NOT NULL, + `repository_id` char(36) NOT NULL, + `parent_revision_id` char(36) NOT NULL DEFAULT '', + `task_id` char(36) NOT NULL, + `revision_kind` varchar(32) NOT NULL, + `status` varchar(32) NOT NULL, + `package_count` int NOT NULL, + `total_bytes` bigint NOT NULL, + `manifest_sha256` char(64) NOT NULL, + `repomd_sha256` char(64) NOT NULL DEFAULT '', + `snapshot_manifest_sha256` char(64) NOT NULL DEFAULT '', + `relative_path` varchar(512) NOT NULL, + `created_by` varchar(256) NOT NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `published_at` datetime(3) NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_repository_revisions_task` (`task_id`), + KEY `idx_repository_revisions_repository` (`repository_id`, `status`, `created_at`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `repository_packages` +( + `id` char(36) NOT NULL, + `repository_id` char(36) NOT NULL, + `revision_id` char(36) NOT NULL, + `name` varchar(255) NOT NULL, + `epoch` varchar(32) NOT NULL DEFAULT '0', + `version` varchar(255) NOT NULL, + `package_release` varchar(255) NOT NULL, + `architecture` varchar(32) NOT NULL, + `nevra` varchar(512) NOT NULL, + `file_name` varchar(255) NOT NULL, + `relative_path` varchar(512) NOT NULL, + `size_bytes` bigint NOT NULL, + `sha256` char(64) NOT NULL, + `package_source` varchar(32) NOT NULL, + `source_upload_id` char(36) NOT NULL DEFAULT '', + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_repository_revision_nevra` (`revision_id`, `nevra`), + KEY `idx_repository_packages_repository` (`repository_id`), + KEY `idx_repository_packages_revision` (`revision_id`), + KEY `idx_repository_packages_name` (`name`), + KEY `idx_repository_packages_arch` (`architecture`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `repository_quarantine_items` +( + `id` char(36) NOT NULL, + `repository_id` char(36) NOT NULL, + `task_id` char(36) NOT NULL, + `upload_id` char(36) NOT NULL DEFAULT '', + `original_name` varchar(255) NOT NULL, + `relative_path` varchar(512) NOT NULL, + `size_bytes` bigint NOT NULL, + `sha256` char(64) NOT NULL, + `reason_code` varchar(64) NOT NULL, + `reason_message` varchar(2048) NOT NULL, + `detected_nevra` varchar(1024) NOT NULL DEFAULT '', + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`), + KEY `idx_repository_quarantine_repository` (`repository_id`), + KEY `idx_repository_quarantine_task` (`task_id`), + KEY `idx_repository_quarantine_reason` (`reason_code`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; diff --git a/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_8_repository_publication.sql b/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_8_repository_publication.sql new file mode 100644 index 0000000000000000000000000000000000000000..604c2a85795c9dc948b7f30277321f962020b50b --- /dev/null +++ b/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_8_repository_publication.sql @@ -0,0 +1,29 @@ +USE `footstone`; + +SET @statement = IF( + EXISTS( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'repository_revisions' + AND column_name = 'repomd_sha256' + ), + 'SELECT 1', + 'ALTER TABLE repository_revisions ADD COLUMN repomd_sha256 char(64) NOT NULL DEFAULT '''' AFTER manifest_sha256' +); +PREPARE repository_publication_migration FROM @statement; +EXECUTE repository_publication_migration; +DEALLOCATE PREPARE repository_publication_migration; + +SET @statement = IF( + EXISTS( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'repository_revisions' + AND column_name = 'snapshot_manifest_sha256' + ), + 'SELECT 1', + 'ALTER TABLE repository_revisions ADD COLUMN snapshot_manifest_sha256 char(64) NOT NULL DEFAULT '''' AFTER repomd_sha256' +); +PREPARE repository_publication_migration FROM @statement; +EXECUTE repository_publication_migration; +DEALLOCATE PREPARE repository_publication_migration; diff --git a/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_9_repository_diagnosis.sql b/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_9_repository_diagnosis.sql new file mode 100644 index 0000000000000000000000000000000000000000..5200f447482dd0b64e6557b850ca18e6d820d9d5 --- /dev/null +++ b/manager/backend/migrations/footstone/sql/mysql/v1.0.0/v1.0.0_9_repository_diagnosis.sql @@ -0,0 +1,44 @@ +USE `footstone`; + +DROP PROCEDURE IF EXISTS `ocm_add_column_if_missing`; +DELIMITER // +CREATE PROCEDURE `ocm_add_column_if_missing`( + IN p_table_name varchar(64), + IN p_column_name varchar(64), + IN p_column_definition text +) +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = p_table_name + AND COLUMN_NAME = p_column_name + ) THEN + SET @ocm_ddl = CONCAT( + 'ALTER TABLE `', p_table_name, '` ADD COLUMN `', + p_column_name, '` ', p_column_definition + ); + PREPARE ocm_stmt FROM @ocm_ddl; + EXECUTE ocm_stmt; + DEALLOCATE PREPARE ocm_stmt; + END IF; +END// +DELIMITER ; + +CALL `ocm_add_column_if_missing`('repository_tasks', 'failure_category', + 'varchar(64) NOT NULL DEFAULT '''' AFTER `error_message`'); +CALL `ocm_add_column_if_missing`('repository_tasks', 'failed_step', + 'varchar(128) NOT NULL DEFAULT '''' AFTER `failure_category`'); +CALL `ocm_add_column_if_missing`('repository_tasks', 'likely_cause', + 'varchar(2048) NOT NULL DEFAULT '''' AFTER `failed_step`'); +CALL `ocm_add_column_if_missing`('repository_tasks', 'diagnosis_evidence', + 'json NULL AFTER `likely_cause`'); +CALL `ocm_add_column_if_missing`('repository_tasks', 'recommended_action', + 'varchar(2048) NOT NULL DEFAULT '''' AFTER `diagnosis_evidence`'); +CALL `ocm_add_column_if_missing`('repository_tasks', 'diagnosis_rule_id', + 'varchar(128) NOT NULL DEFAULT '''' AFTER `recommended_action`'); +CALL `ocm_add_column_if_missing`('repository_tasks', 'diagnosis_generated_at', + 'datetime(3) NULL AFTER `diagnosis_rule_id`'); + +DROP PROCEDURE `ocm_add_column_if_missing`; diff --git a/manager/backend/pkg/plugin/permission/plugin.go b/manager/backend/pkg/plugin/permission/plugin.go index 4af1cb52e7ac71e7dea72d6a394e1850c02d9509..1e1f3406af1067bc148626224c5fb6e9bcf4b0fa 100644 --- a/manager/backend/pkg/plugin/permission/plugin.go +++ b/manager/backend/pkg/plugin/permission/plugin.go @@ -114,6 +114,7 @@ func (p *TMPlugin) InitConfig(configDec plugin.Decoder) (cfg Config, err error) PermissionService: ps, AuthService: auth, AuditService: audit, + JWTConfig: jwtConfigFromEnvironment(), Path: NewPathController(), Debug: op.Debug, GatewaySecret: []byte(op.GatewaySecret), @@ -170,6 +171,18 @@ type JWTConfig struct { JWTHeadKey string } +func jwtConfigFromEnvironment() JWTConfig { + secret := strings.TrimSpace(os.Getenv("JWT_SECRET")) + if secret == "" { + return JWTConfig{} + } + headKey := strings.TrimSpace(os.Getenv("JWT_HEAD_KEY")) + if headKey == "" { + headKey = "Authorization" + } + return JWTConfig{JWTSecret: []byte(secret), JWTHeadKey: headKey} +} + // GetJWTSecret @Description: 获取jwt的token func (c *Config) GetJWTSecret() (conf JWTConfig, err error) { c.jwtMu.Lock() diff --git a/manager/backend/pkg/plugin/permission/plugin_test.go b/manager/backend/pkg/plugin/permission/plugin_test.go index ec744ac3713748fcf40037892bae15b74e4b0e6a..61359655407067f1040c3e002577ef2ebf39600b 100644 --- a/manager/backend/pkg/plugin/permission/plugin_test.go +++ b/manager/backend/pkg/plugin/permission/plugin_test.go @@ -90,6 +90,27 @@ func TestLoadJWTFromEnv(t *testing.T) { } } +func TestJWTConfigFromEnvironment(t *testing.T) { + t.Setenv("JWT_SECRET", " local-secret ") + t.Setenv("JWT_HEAD_KEY", "") + + got := jwtConfigFromEnvironment() + if string(got.JWTSecret) != "local-secret" || got.JWTHeadKey != "Authorization" { + t.Fatalf("jwtConfigFromEnvironment() = %+v", got) + } +} + +func TestConfigGetJWTSecretUsesLocalConfig(t *testing.T) { + cfg := Config{JWTConfig: JWTConfig{JWTSecret: []byte("local-secret"), JWTHeadKey: "X-Auth"}} + got, err := cfg.GetJWTSecret() + if err != nil { + t.Fatalf("GetJWTSecret() error = %v", err) + } + if string(got.JWTSecret) != "local-secret" || got.JWTHeadKey != "X-Auth" { + t.Fatalf("GetJWTSecret() = %+v", got) + } +} + func TestConfigGetJWTSecretNilResponse(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() diff --git a/manager/backend/protocol/Makefile b/manager/backend/protocol/Makefile index 6d65d501e1b2d9e7ca0a747ea34168ceaccdf4e9..115aad3708965d359406885502b171fc1d362fe1 100644 --- a/manager/backend/protocol/Makefile +++ b/manager/backend/protocol/Makefile @@ -1,8 +1,15 @@ build: ./build.sh +.PHONY: repository repositorynode +repository: + $(MAKE) -C repository build + +repositorynode: + $(MAKE) -C repositorynode build + clean: find . -name "*.go" | xargs rm -rf find . -name "*.json" | xargs rm -rf find . -name "go.mod" | xargs rm -rf - find . -name "go.sum" | xargs rm -rf \ No newline at end of file + find . -name "go.sum" | xargs rm -rf diff --git a/manager/backend/protocol/README.md b/manager/backend/protocol/README.md index 531f98e08ea759976cd4c4f872785f7c65aff562..dc6315df167c402eff137fc37342f3a92fbe218f 100644 --- a/manager/backend/protocol/README.md +++ b/manager/backend/protocol/README.md @@ -56,3 +56,8 @@ swagger,接口说明见 [`docs/reference/api-reference.md`](../../../docs/refe - 目前生成物与 `google.golang.org/protobuf` 当前版本可能存在运行时 init 告警,建议在工具链就绪后通过 `./build.sh` 重新生成以消除。 - `Makefile` 中的默认工具路径假设开发机已安装 `trpc` 与 `protoc`;若执行失败请先确认工具版本。 +Repository 协议必须使用独立目标生成,避免重写无关协议: + +```bash +make repository +``` diff --git a/manager/backend/protocol/repository/Makefile b/manager/backend/protocol/repository/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..f2ee2dbc5e7167900d8a6d19439657876bed017f --- /dev/null +++ b/manager/backend/protocol/repository/Makefile @@ -0,0 +1,4 @@ +.PHONY: build + +build: + bash ./build.sh diff --git a/manager/backend/protocol/repository/apidocs.swagger.json b/manager/backend/protocol/repository/apidocs.swagger.json new file mode 100644 index 0000000000000000000000000000000000000000..65352ab1523fbf750a8164375c561854d5a873fe --- /dev/null +++ b/manager/backend/protocol/repository/apidocs.swagger.json @@ -0,0 +1,7269 @@ +{ + "swagger": "2.0", + "info": { + "title": "repository", + "description": "The api document of repository.proto", + "version": "2.0" + }, + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/trpc.ocmanager.repository.RepositoryService/AbortRepositoryUpload": { + "post": { + "summary": "", + "operationId": "AbortRepositoryUpload", + "responses": { + "200": { + "description": "RepositoryUploadResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryUploadResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.AbortRepositoryUploadRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/AppendRepositoryOCAIFeedback": { + "post": { + "summary": "", + "operationId": "AppendRepositoryOCAIFeedback", + "responses": { + "200": { + "description": "RepositoryOCAIAnalysisResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryOCAIAnalysisResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.AppendRepositoryOCAIFeedbackRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/AppendRepositoryUploadChunk": { + "post": { + "summary": "", + "operationId": "AppendRepositoryUploadChunk", + "responses": { + "200": { + "description": "RepositoryUploadResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryUploadResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.AppendRepositoryUploadChunkRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/BatchApplyRepositories": { + "post": { + "summary": "", + "operationId": "BatchApplyRepositories", + "responses": { + "200": { + "description": "BatchApplyRepositoriesResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.BatchApplyRepositoriesResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.BatchApplyRepositoriesRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/BeginRepositoryUpload": { + "post": { + "summary": "", + "operationId": "BeginRepositoryUpload", + "responses": { + "200": { + "description": "RepositoryUploadResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryUploadResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.BeginRepositoryUploadRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/BuildRepositoryOCAIContext": { + "post": { + "summary": "", + "operationId": "BuildRepositoryOCAIContext", + "responses": { + "200": { + "description": "RepositoryOCAIContextResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryOCAIContextResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.BuildRepositoryOCAIContextRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/CancelRepositoryTask": { + "post": { + "summary": "", + "operationId": "CancelRepositoryTask", + "responses": { + "200": { + "description": "RepositoryTaskResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryTaskResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.CancelRepositoryTaskRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/CompleteRepositoryUpload": { + "post": { + "summary": "", + "operationId": "CompleteRepositoryUpload", + "responses": { + "200": { + "description": "CompleteRepositoryUploadResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.CompleteRepositoryUploadResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.CompleteRepositoryUploadRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/ConfigureRepositoryNodeInstallSource": { + "post": { + "summary": "", + "operationId": "ConfigureRepositoryNodeInstallSource", + "responses": { + "200": { + "description": "RepositoryNodeInstallSourceResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryNodeInstallSourceResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ConfigureRepositoryNodeInstallSourceRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/ConfirmRepositoryInventoryImport": { + "post": { + "summary": "", + "operationId": "ConfirmRepositoryInventoryImport", + "responses": { + "200": { + "description": "RepositoryInventoryImportResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryInventoryImportResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ConfirmRepositoryInventoryImportRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/ConfirmRepositoryPackageDeletion": { + "post": { + "summary": "", + "operationId": "ConfirmRepositoryPackageDeletion", + "responses": { + "200": { + "description": "RepositoryTaskResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryTaskResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ConfirmRepositoryPackageDeletionRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/ConfirmRepositoryPackageInstall": { + "post": { + "summary": "", + "operationId": "ConfirmRepositoryPackageInstall", + "responses": { + "200": { + "description": "RepositoryPackageInstallResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryPackageInstallResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ConfirmRepositoryPackageInstallRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/CreateRepository": { + "post": { + "summary": "", + "operationId": "CreateRepository", + "responses": { + "200": { + "description": "RepositoryResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.CreateRepositoryRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/CreateRepositoryDistribution": { + "post": { + "summary": "", + "operationId": "CreateRepositoryDistribution", + "responses": { + "200": { + "description": "RepositoryDistributionResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryDistributionResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.CreateRepositoryDistributionRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/CreateRepositoryNodeEnrollment": { + "post": { + "summary": "", + "operationId": "CreateRepositoryNodeEnrollment", + "responses": { + "200": { + "description": "RepositoryNodeEnrollmentResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryNodeEnrollmentResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.CreateRepositoryNodeEnrollmentRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/CreateRepositoryOCAIAnalysis": { + "post": { + "summary": "", + "operationId": "CreateRepositoryOCAIAnalysis", + "responses": { + "200": { + "description": "RepositoryOCAIAnalysisResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryOCAIAnalysisResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.CreateRepositoryOCAIAnalysisRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/CreateRepositoryStorageNode": { + "post": { + "summary": "", + "operationId": "CreateRepositoryStorageNode", + "responses": { + "200": { + "description": "RepositoryStorageNodeResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryStorageNodeResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.CreateRepositoryStorageNodeRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/CreateRepositoryUpstreamProfile": { + "post": { + "summary": "", + "operationId": "CreateRepositoryUpstreamProfile", + "responses": { + "200": { + "description": "RepositoryUpstreamProfileResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryUpstreamProfileResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.CreateRepositoryUpstreamProfileRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/DeleteRepository": { + "post": { + "summary": "", + "operationId": "DeleteRepository", + "responses": { + "200": { + "description": "A generic empty message that you can re-use to avoid defining duplicated\n empty messages in your APIs. A typical example is to use it as the request\n or the response type of an API method. For instance:\n\n service Foo {\n rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty);\n }\n\n The JSON representation for `Empty` is empty JSON object `{}`.", + "schema": { + "$ref": "#/definitions/google.protobuf.Empty" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.DeleteRepositoryRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/DeleteRepositoryGPGKey": { + "post": { + "summary": "", + "operationId": "DeleteRepositoryGPGKey", + "responses": { + "200": { + "description": "A generic empty message that you can re-use to avoid defining duplicated\n empty messages in your APIs. A typical example is to use it as the request\n or the response type of an API method. For instance:\n\n service Foo {\n rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty);\n }\n\n The JSON representation for `Empty` is empty JSON object `{}`.", + "schema": { + "$ref": "#/definitions/google.protobuf.Empty" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.DeleteRepositoryGPGKeyRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/DeleteRepositoryPackage": { + "post": { + "summary": "", + "operationId": "DeleteRepositoryPackage", + "responses": { + "200": { + "description": "RepositoryTaskResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryTaskResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.DeleteRepositoryPackageRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/DeleteRepositoryStorageNode": { + "post": { + "summary": "", + "operationId": "DeleteRepositoryStorageNode", + "responses": { + "200": { + "description": "A generic empty message that you can re-use to avoid defining duplicated\n empty messages in your APIs. A typical example is to use it as the request\n or the response type of an API method. For instance:\n\n service Foo {\n rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty);\n }\n\n The JSON representation for `Empty` is empty JSON object `{}`.", + "schema": { + "$ref": "#/definitions/google.protobuf.Empty" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.DeleteRepositoryStorageNodeRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/DeleteRepositoryUpstreamProfile": { + "post": { + "summary": "", + "operationId": "DeleteRepositoryUpstreamProfile", + "responses": { + "200": { + "description": "A generic empty message that you can re-use to avoid defining duplicated\n empty messages in your APIs. A typical example is to use it as the request\n or the response type of an API method. For instance:\n\n service Foo {\n rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty);\n }\n\n The JSON representation for `Empty` is empty JSON object `{}`.", + "schema": { + "$ref": "#/definitions/google.protobuf.Empty" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.DeleteRepositoryUpstreamProfileRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/DescribeRepository": { + "post": { + "summary": "", + "operationId": "DescribeRepository", + "responses": { + "200": { + "description": "RepositoryResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.DescribeRepositoryRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/DescribeRepositoryDistribution": { + "post": { + "summary": "", + "operationId": "DescribeRepositoryDistribution", + "responses": { + "200": { + "description": "RepositoryDistributionResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryDistributionResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.DescribeRepositoryDistributionRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/DescribeRepositoryOCAIAnalysis": { + "post": { + "summary": "", + "operationId": "DescribeRepositoryOCAIAnalysis", + "responses": { + "200": { + "description": "RepositoryOCAIAnalysisResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryOCAIAnalysisResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.DescribeRepositoryOCAIAnalysisRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/DescribeRepositoryPackageInstall": { + "post": { + "summary": "", + "operationId": "DescribeRepositoryPackageInstall", + "responses": { + "200": { + "description": "RepositoryPackageInstallResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryPackageInstallResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.DescribeRepositoryPackageInstallRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/DescribeRepositoryStorageNode": { + "post": { + "summary": "", + "operationId": "DescribeRepositoryStorageNode", + "responses": { + "200": { + "description": "RepositoryStorageNodeResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryStorageNodeResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.DescribeRepositoryStorageNodeRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/DescribeRepositorySyncSchedule": { + "post": { + "summary": "", + "operationId": "DescribeRepositorySyncSchedule", + "responses": { + "200": { + "description": "RepositorySyncScheduleResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositorySyncScheduleResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.DescribeRepositorySyncScheduleRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/DescribeRepositoryTask": { + "post": { + "summary": "", + "operationId": "DescribeRepositoryTask", + "responses": { + "200": { + "description": "RepositoryTaskResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryTaskResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.DescribeRepositoryTaskRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/DescribeRepositoryTaskLog": { + "post": { + "summary": "", + "operationId": "DescribeRepositoryTaskLog", + "responses": { + "200": { + "description": "RepositoryTaskLogResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryTaskLogResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.DescribeRepositoryTaskLogRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/DescribeRepositoryUpload": { + "post": { + "summary": "", + "operationId": "DescribeRepositoryUpload", + "responses": { + "200": { + "description": "RepositoryUploadResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryUploadResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.DescribeRepositoryUploadRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/DescribeRepositoryUploadChunks": { + "post": { + "summary": "", + "operationId": "DescribeRepositoryUploadChunks", + "responses": { + "200": { + "description": "DescribeRepositoryUploadChunksResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.DescribeRepositoryUploadChunksResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.DescribeRepositoryUploadRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/DescribeRepositoryUpstreamProfile": { + "post": { + "summary": "", + "operationId": "DescribeRepositoryUpstreamProfile", + "responses": { + "200": { + "description": "RepositoryUpstreamProfileResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryUpstreamProfileResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.DescribeRepositoryUpstreamProfileRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/DownloadRepositoryNodeArtifact": { + "post": { + "summary": "", + "operationId": "DownloadRepositoryNodeArtifact", + "responses": { + "200": { + "description": "A generic empty message that you can re-use to avoid defining duplicated\n empty messages in your APIs. A typical example is to use it as the request\n or the response type of an API method. For instance:\n\n service Foo {\n rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty);\n }\n\n The JSON representation for `Empty` is empty JSON object `{}`.", + "schema": { + "$ref": "#/definitions/google.protobuf.Empty" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.DownloadRepositoryNodeArtifactRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/DownloadRepositoryTaskLog": { + "post": { + "summary": "", + "operationId": "DownloadRepositoryTaskLog", + "responses": { + "200": { + "description": "A generic empty message that you can re-use to avoid defining duplicated\n empty messages in your APIs. A typical example is to use it as the request\n or the response type of an API method. For instance:\n\n service Foo {\n rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty);\n }\n\n The JSON representation for `Empty` is empty JSON object `{}`.", + "schema": { + "$ref": "#/definitions/google.protobuf.Empty" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.DownloadRepositoryTaskLogRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/ExportRepositoryDistributionResult": { + "post": { + "summary": "", + "operationId": "ExportRepositoryDistributionResult", + "responses": { + "200": { + "description": "RepositoryExportResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryExportResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ExportRepositoryDistributionResultRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/ExportRepositoryInventory": { + "post": { + "summary": "", + "operationId": "ExportRepositoryInventory", + "responses": { + "200": { + "description": "RepositoryExportResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryExportResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ExportRepositoryInventoryRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/ExportRepositoryTaskDiagnosis": { + "post": { + "summary": "", + "operationId": "ExportRepositoryTaskDiagnosis", + "responses": { + "200": { + "description": "RepositoryDiagnosisReportResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryDiagnosisReportResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ExportRepositoryTaskDiagnosisRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/ExportRepositoryValidationReport": { + "post": { + "summary": "", + "operationId": "ExportRepositoryValidationReport", + "responses": { + "200": { + "description": "RepositoryExportResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryExportResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ExportRepositoryValidationReportRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/GenerateRepositoryConfig": { + "post": { + "summary": "", + "operationId": "GenerateRepositoryConfig", + "responses": { + "200": { + "description": "RepositoryConfigResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryConfigResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.GenerateRepositoryConfigRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/ListRepositories": { + "post": { + "summary": "", + "operationId": "ListRepositories", + "responses": { + "200": { + "description": "ListRepositoriesResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ListRepositoriesResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ListRepositoriesRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/ListRepositoryDistributionNodes": { + "post": { + "summary": "", + "operationId": "ListRepositoryDistributionNodes", + "responses": { + "200": { + "description": "ListRepositoryDistributionNodesResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ListRepositoryDistributionNodesResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ListRepositoryDistributionNodesRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/ListRepositoryDistributions": { + "post": { + "summary": "", + "operationId": "ListRepositoryDistributions", + "responses": { + "200": { + "description": "ListRepositoryDistributionsResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ListRepositoryDistributionsResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ListRepositoryDistributionsRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/ListRepositoryGPGKeys": { + "post": { + "summary": "", + "operationId": "ListRepositoryGPGKeys", + "responses": { + "200": { + "description": "ListRepositoryGPGKeysResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ListRepositoryGPGKeysResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ListRepositoryGPGKeysRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/ListRepositoryNodeArtifacts": { + "post": { + "summary": "", + "operationId": "ListRepositoryNodeArtifacts", + "responses": { + "200": { + "description": "ListRepositoryNodeArtifactsResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ListRepositoryNodeArtifactsResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ListRepositoryNodeArtifactsRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/ListRepositoryNodeInstallSources": { + "post": { + "summary": "", + "operationId": "ListRepositoryNodeInstallSources", + "responses": { + "200": { + "description": "ListRepositoryNodeInstallSourcesResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ListRepositoryNodeInstallSourcesResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ListRepositoryNodeInstallSourcesRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/ListRepositoryNodeRegistrationAllowlists": { + "post": { + "summary": "", + "operationId": "ListRepositoryNodeRegistrationAllowlists", + "responses": { + "200": { + "description": "ListRepositoryNodeRegistrationAllowlistsResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ListRepositoryNodeRegistrationAllowlistsResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ListRepositoryNodeRegistrationAllowlistsRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/ListRepositoryPackages": { + "post": { + "summary": "", + "operationId": "ListRepositoryPackages", + "responses": { + "200": { + "description": "ListRepositoryPackagesResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ListRepositoryPackagesResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ListRepositoryPackagesRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/ListRepositoryPreflightRecords": { + "post": { + "summary": "", + "operationId": "ListRepositoryPreflightRecords", + "responses": { + "200": { + "description": "ListRepositoryPreflightRecordsResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ListRepositoryPreflightRecordsResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ListRepositoryPreflightRecordsRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/ListRepositoryQuarantine": { + "post": { + "summary": "", + "operationId": "ListRepositoryQuarantine", + "responses": { + "200": { + "description": "ListRepositoryQuarantineResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ListRepositoryQuarantineResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ListRepositoryQuarantineRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/ListRepositoryStorageNodes": { + "post": { + "summary": "", + "operationId": "ListRepositoryStorageNodes", + "responses": { + "200": { + "description": "ListRepositoryStorageNodesResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ListRepositoryStorageNodesResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ListRepositoryStorageNodesRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/ListRepositoryTasks": { + "post": { + "summary": "", + "operationId": "ListRepositoryTasks", + "responses": { + "200": { + "description": "ListRepositoryTasksResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ListRepositoryTasksResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ListRepositoryTasksRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/ListRepositoryUpstreamProfiles": { + "post": { + "summary": "", + "operationId": "ListRepositoryUpstreamProfiles", + "responses": { + "200": { + "description": "ListRepositoryUpstreamProfilesResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ListRepositoryUpstreamProfilesResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ListRepositoryUpstreamProfilesRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/MigrateRepository": { + "post": { + "summary": "", + "operationId": "MigrateRepository", + "responses": { + "200": { + "description": "RepositoryTaskResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryTaskResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.MigrateRepositoryRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/PauseRepositorySyncSchedule": { + "post": { + "summary": "", + "operationId": "PauseRepositorySyncSchedule", + "responses": { + "200": { + "description": "RepositorySyncScheduleResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositorySyncScheduleResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ChangeRepositorySyncScheduleStatusRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/PauseRepositoryUpload": { + "post": { + "summary": "", + "operationId": "PauseRepositoryUpload", + "responses": { + "200": { + "description": "RepositoryUploadResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryUploadResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.DescribeRepositoryUploadRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/PreviewRepositoryInventoryImport": { + "post": { + "summary": "", + "operationId": "PreviewRepositoryInventoryImport", + "responses": { + "200": { + "description": "RepositoryInventoryImportPreviewResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryInventoryImportPreviewResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.PreviewRepositoryInventoryImportRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/PreviewRepositoryPackageDeletion": { + "post": { + "summary": "", + "operationId": "PreviewRepositoryPackageDeletion", + "responses": { + "200": { + "description": "RepositoryPackageDeletionPreviewResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryPackageDeletionPreviewResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.PreviewRepositoryPackageDeletionRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/PreviewRepositoryPackageInstall": { + "post": { + "summary": "", + "operationId": "PreviewRepositoryPackageInstall", + "responses": { + "200": { + "description": "RepositoryPackageInstallPreviewResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryPackageInstallPreviewResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.PreviewRepositoryPackageInstallRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/ProbeRepositoryStorageNode": { + "post": { + "summary": "", + "operationId": "ProbeRepositoryStorageNode", + "responses": { + "200": { + "description": "RepositoryStorageNodeProbeResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryStorageNodeProbeResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ProbeRepositoryStorageNodeRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/PutRepositoryUploadChunk": { + "post": { + "summary": "", + "operationId": "PutRepositoryUploadChunk", + "responses": { + "200": { + "description": "PutRepositoryUploadChunkResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.PutRepositoryUploadChunkResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.PutRepositoryUploadChunkRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/RegisterRepositoryGPGKey": { + "post": { + "summary": "", + "operationId": "RegisterRepositoryGPGKey", + "responses": { + "200": { + "description": "RepositoryGPGKeyResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryGPGKeyResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RegisterRepositoryGPGKeyRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/RegisterRepositoryNodeArtifact": { + "post": { + "summary": "", + "operationId": "RegisterRepositoryNodeArtifact", + "responses": { + "200": { + "description": "RepositoryNodeArtifactResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryNodeArtifactResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RegisterRepositoryNodeArtifactRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/ResumeRepositorySyncSchedule": { + "post": { + "summary": "", + "operationId": "ResumeRepositorySyncSchedule", + "responses": { + "200": { + "description": "RepositorySyncScheduleResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositorySyncScheduleResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.ChangeRepositorySyncScheduleStatusRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/ResumeRepositoryUpload": { + "post": { + "summary": "", + "operationId": "ResumeRepositoryUpload", + "responses": { + "200": { + "description": "RepositoryUploadResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryUploadResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.DescribeRepositoryUploadRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/RetryRepositoryTask": { + "post": { + "summary": "", + "operationId": "RetryRepositoryTask", + "responses": { + "200": { + "description": "RepositoryTaskResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryTaskResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RetryRepositoryTaskRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/SyncRepository": { + "post": { + "summary": "", + "operationId": "SyncRepository", + "responses": { + "200": { + "description": "RepositoryTaskResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryTaskResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.SyncRepositoryRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/UpdateRepository": { + "post": { + "summary": "", + "operationId": "UpdateRepository", + "responses": { + "200": { + "description": "RepositoryResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.UpdateRepositoryRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/UpdateRepositoryStorageNode": { + "post": { + "summary": "", + "operationId": "UpdateRepositoryStorageNode", + "responses": { + "200": { + "description": "RepositoryStorageNodeResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryStorageNodeResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.UpdateRepositoryStorageNodeRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/UpdateRepositoryUpstreamProfile": { + "post": { + "summary": "", + "operationId": "UpdateRepositoryUpstreamProfile", + "responses": { + "200": { + "description": "RepositoryUpstreamProfileResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryUpstreamProfileResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.UpdateRepositoryUpstreamProfileRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/UpsertRepositoryNodeRegistrationAllowlist": { + "post": { + "summary": "", + "operationId": "UpsertRepositoryNodeRegistrationAllowlist", + "responses": { + "200": { + "description": "RepositoryNodeRegistrationAllowlistResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryNodeRegistrationAllowlistResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.UpsertRepositoryNodeRegistrationAllowlistRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + }, + "/trpc.ocmanager.repository.RepositoryService/UpsertRepositorySyncSchedule": { + "post": { + "summary": "", + "operationId": "UpsertRepositorySyncSchedule", + "responses": { + "200": { + "description": "RepositorySyncScheduleResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositorySyncScheduleResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repository.UpsertRepositorySyncScheduleRequest" + } + } + ], + "tags": [ + "repositoryservice.trpc" + ] + } + } + }, + "definitions": { + "google.protobuf.Empty": { + "type": "object", + "title": "google.protobuf.Empty", + "description": "A generic empty message that you can re-use to avoid defining duplicated\n empty messages in your APIs. A typical example is to use it as the request\n or the response type of an API method. For instance:\n\n service Foo {\n rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty);\n }\n\n The JSON representation for `Empty` is empty JSON object `{}`." + }, + "google.protobuf.Timestamp": { + "type": "object", + "properties": { + "nanos": { + "title": "nanos", + "type": "integer", + "format": "int32", + "description": "Non-negative fractions of a second at nanosecond resolution. Negative\n second values with fractions must still have non-negative nanos values\n that count forward in time. Must be from 0 to 999,999,999\n inclusive." + }, + "seconds": { + "title": "seconds", + "type": "string", + "format": "int64", + "description": "Represents seconds of UTC time since Unix epoch\n 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to\n 9999-12-31T23:59:59Z inclusive." + } + }, + "title": "google.protobuf.Timestamp", + "description": "A Timestamp represents a point in time independent of any time zone or local\n calendar, encoded as a count of seconds and fractions of seconds at\n nanosecond resolution. The count is relative to an epoch at UTC midnight on\n January 1, 1970, in the proleptic Gregorian calendar which extends the\n Gregorian calendar backwards to year one.\n\n All minutes are 60 seconds long. Leap seconds are \"smeared\" so that no leap\n second table is needed for interpretation, using a [24-hour linear\n smear](https://developers.google.com/time/smear).\n\n The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By\n restricting to that range, we ensure that we can convert to and from [RFC\n 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings.\n\n # Examples\n\n Example 1: Compute Timestamp from POSIX `time()`.\n\n Timestamp timestamp;\n timestamp.set_seconds(time(NULL));\n timestamp.set_nanos(0);\n\n Example 2: Compute Timestamp from POSIX `gettimeofday()`.\n\n struct timeval tv;\n gettimeofday(\u0026tv, NULL);\n\n Timestamp timestamp;\n timestamp.set_seconds(tv.tv_sec);\n timestamp.set_nanos(tv.tv_usec * 1000);\n\n Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`.\n\n FILETIME ft;\n GetSystemTimeAsFileTime(\u0026ft);\n UINT64 ticks = (((UINT64)ft.dwHighDateTime) \u003c\u003c 32) | ft.dwLowDateTime;\n\n // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z\n // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z.\n Timestamp timestamp;\n timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL));\n timestamp.set_nanos((INT32) ((ticks % 10000000) * 100));\n\n Example 4: Compute Timestamp from Java `System.currentTimeMillis()`.\n\n long millis = System.currentTimeMillis();\n\n Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000)\n .setNanos((int) ((millis % 1000) * 1000000)).build();\n\n\n Example 5: Compute Timestamp from Java `Instant.now()`.\n\n Instant now = Instant.now();\n\n Timestamp timestamp =\n Timestamp.newBuilder().setSeconds(now.getEpochSecond())\n .setNanos(now.getNano()).build();\n\n\n Example 6: Compute Timestamp from current time in Python.\n\n timestamp = Timestamp()\n timestamp.GetCurrentTime()\n\n # JSON Mapping\n\n In JSON format, the Timestamp type is encoded as a string in the\n [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the\n format is \"{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z\"\n where {year} is always expressed using four digits while {month}, {day},\n {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional\n seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution),\n are optional. The \"Z\" suffix indicates the timezone (\"UTC\"); the timezone\n is required. A proto3 JSON serializer should always use UTC (as indicated by\n \"Z\") when printing the Timestamp type and a proto3 JSON parser should be\n able to accept both UTC and other timezones (as indicated by an offset).\n\n For example, \"2017-01-15T01:30:15.01Z\" encodes 15.01 seconds past\n 01:30 UTC on January 15, 2017.\n\n In JavaScript, one can convert a Date object to this format using the\n standard\n [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)\n method. In Python, a standard `datetime.datetime` object can be converted\n to this format using\n [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with\n the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use\n the Joda Time's [`ISODateTimeFormat.dateTime()`](\n http://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime%2D%2D\n ) to obtain a formatter capable of generating timestamps in this format." + }, + "trpc.ocmanager.common.PageRequest": { + "type": "object", + "properties": { + "order": { + "title": "order", + "type": "string" + }, + "order_by": { + "title": "order_by", + "type": "string", + "description": "desc asc" + }, + "page_number": { + "title": "page_number", + "type": "string", + "format": "int64" + }, + "page_size": { + "title": "page_size", + "type": "string", + "format": "int64" + } + }, + "title": "trpc.ocmanager.common.PageRequest", + "description": "PageRequest" + }, + "trpc.ocmanager.common.PageResponse": { + "type": "object", + "properties": { + "count": { + "title": "count", + "type": "string", + "format": "int64" + }, + "page_count": { + "title": "page_count", + "type": "string", + "format": "int64" + }, + "page_number": { + "title": "page_number", + "type": "string", + "format": "int64" + }, + "page_size": { + "title": "page_size", + "type": "string", + "format": "int64" + } + }, + "title": "trpc.ocmanager.common.PageResponse", + "description": "PageResponse" + }, + "trpc.ocmanager.repository.AbortRepositoryUploadRequest": { + "type": "object", + "properties": { + "upload_id": { + "title": "upload_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.AbortRepositoryUploadRequest", + "description": "AbortRepositoryUploadRequest" + }, + "trpc.ocmanager.repository.AppendRepositoryOCAIFeedbackRequest": { + "type": "object", + "properties": { + "action_result": { + "title": "action_result", + "type": "string" + }, + "action_task_id": { + "title": "action_task_id", + "type": "string" + }, + "session_id": { + "title": "session_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.AppendRepositoryOCAIFeedbackRequest", + "description": "AppendRepositoryOCAIFeedbackRequest" + }, + "trpc.ocmanager.repository.AppendRepositoryUploadChunkRequest": { + "type": "object", + "properties": { + "data": { + "title": "data", + "type": "string", + "format": "bytes" + }, + "offset": { + "title": "offset", + "type": "string", + "format": "int64" + }, + "upload_id": { + "title": "upload_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.AppendRepositoryUploadChunkRequest", + "description": "AppendRepositoryUploadChunkRequest" + }, + "trpc.ocmanager.repository.BatchApplyRepositoriesRequest": { + "type": "object", + "properties": { + "items": { + "title": "items", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.BatchApplyRepositoryItem" + } + } + }, + "title": "trpc.ocmanager.repository.BatchApplyRepositoriesRequest", + "description": "BatchApplyRepositoriesRequest" + }, + "trpc.ocmanager.repository.BatchApplyRepositoriesResponse": { + "type": "object", + "properties": { + "results": { + "title": "results", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.BatchApplyRepositoryResult" + } + } + }, + "title": "trpc.ocmanager.repository.BatchApplyRepositoriesResponse", + "description": "BatchApplyRepositoriesResponse" + }, + "trpc.ocmanager.repository.BatchApplyRepositoryItem": { + "type": "object", + "properties": { + "repository_id": { + "title": "repository_id", + "type": "string", + "description": "Empty repository_id creates a repository; a non-empty ID updates it." + }, + "spec": { + "title": "spec", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositorySpec" + } + }, + "title": "trpc.ocmanager.repository.BatchApplyRepositoryItem", + "description": "BatchApplyRepositoryItem" + }, + "trpc.ocmanager.repository.BatchApplyRepositoryResult": { + "type": "object", + "properties": { + "input_index": { + "title": "input_index", + "type": "integer", + "format": "int32" + }, + "operation": { + "title": "operation", + "type": "string" + }, + "repository": { + "title": "repository", + "$ref": "#/definitions/trpc.ocmanager.repository.Repository" + } + }, + "title": "trpc.ocmanager.repository.BatchApplyRepositoryResult", + "description": "BatchApplyRepositoryResult" + }, + "trpc.ocmanager.repository.BeginRepositoryUploadRequest": { + "type": "object", + "properties": { + "chunk_size": { + "title": "chunk_size", + "type": "integer", + "format": "int32" + }, + "client_fingerprint": { + "title": "client_fingerprint", + "type": "string" + }, + "expected_sha256": { + "title": "expected_sha256", + "type": "string" + }, + "file_name": { + "title": "file_name", + "type": "string" + }, + "repository_id": { + "title": "repository_id", + "type": "string" + }, + "total_size": { + "title": "total_size", + "type": "string", + "format": "int64" + } + }, + "title": "trpc.ocmanager.repository.BeginRepositoryUploadRequest", + "description": "BeginRepositoryUploadRequest" + }, + "trpc.ocmanager.repository.BuildRepositoryOCAIContextRequest": { + "type": "object", + "properties": { + "task_id": { + "title": "task_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.BuildRepositoryOCAIContextRequest", + "description": "BuildRepositoryOCAIContextRequest" + }, + "trpc.ocmanager.repository.CancelRepositoryTaskRequest": { + "type": "object", + "properties": { + "task_id": { + "title": "task_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.CancelRepositoryTaskRequest", + "description": "CancelRepositoryTaskRequest" + }, + "trpc.ocmanager.repository.ChangeRepositorySyncScheduleStatusRequest": { + "type": "object", + "properties": { + "repository_id": { + "title": "repository_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.ChangeRepositorySyncScheduleStatusRequest", + "description": "ChangeRepositorySyncScheduleStatusRequest" + }, + "trpc.ocmanager.repository.CompleteRepositoryUploadRequest": { + "type": "object", + "properties": { + "upload_id": { + "title": "upload_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.CompleteRepositoryUploadRequest", + "description": "CompleteRepositoryUploadRequest" + }, + "trpc.ocmanager.repository.CompleteRepositoryUploadResponse": { + "type": "object", + "properties": { + "task": { + "title": "task", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryTask" + }, + "upload": { + "title": "upload", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryUpload" + } + }, + "title": "trpc.ocmanager.repository.CompleteRepositoryUploadResponse", + "description": "CompleteRepositoryUploadResponse" + }, + "trpc.ocmanager.repository.ConfigureRepositoryNodeInstallSourceRequest": { + "type": "object", + "properties": { + "spec": { + "title": "spec", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryNodeInstallSourceSpec" + } + }, + "title": "trpc.ocmanager.repository.ConfigureRepositoryNodeInstallSourceRequest", + "description": "ConfigureRepositoryNodeInstallSourceRequest" + }, + "trpc.ocmanager.repository.ConfirmRepositoryInventoryImportRequest": { + "type": "object", + "properties": { + "preview_digest": { + "title": "preview_digest", + "type": "string" + }, + "preview_id": { + "title": "preview_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.ConfirmRepositoryInventoryImportRequest", + "description": "ConfirmRepositoryInventoryImportRequest" + }, + "trpc.ocmanager.repository.ConfirmRepositoryPackageDeletionRequest": { + "type": "object", + "properties": { + "preview_digest": { + "title": "preview_digest", + "type": "string" + }, + "preview_id": { + "title": "preview_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.ConfirmRepositoryPackageDeletionRequest", + "description": "ConfirmRepositoryPackageDeletionRequest" + }, + "trpc.ocmanager.repository.ConfirmRepositoryPackageInstallRequest": { + "type": "object", + "properties": { + "preview_digest": { + "title": "preview_digest", + "type": "string" + }, + "preview_id": { + "title": "preview_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.ConfirmRepositoryPackageInstallRequest", + "description": "ConfirmRepositoryPackageInstallRequest" + }, + "trpc.ocmanager.repository.CreateRepositoryDistributionRequest": { + "type": "object", + "properties": { + "repository_id": { + "title": "repository_id", + "type": "string" + }, + "revision_id": { + "title": "revision_id", + "type": "string" + }, + "target_ips": { + "title": "target_ips", + "type": "array", + "items": { + "type": "string" + } + }, + "timeout_seconds": { + "title": "timeout_seconds", + "type": "integer", + "format": "int32" + } + }, + "title": "trpc.ocmanager.repository.CreateRepositoryDistributionRequest", + "description": "CreateRepositoryDistributionRequest" + }, + "trpc.ocmanager.repository.CreateRepositoryNodeEnrollmentRequest": { + "type": "object", + "properties": { + "spec": { + "title": "spec", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryNodeEnrollmentSpec" + } + }, + "title": "trpc.ocmanager.repository.CreateRepositoryNodeEnrollmentRequest", + "description": "CreateRepositoryNodeEnrollmentRequest" + }, + "trpc.ocmanager.repository.CreateRepositoryOCAIAnalysisRequest": { + "type": "object", + "properties": { + "task_id": { + "title": "task_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.CreateRepositoryOCAIAnalysisRequest", + "description": "CreateRepositoryOCAIAnalysisRequest" + }, + "trpc.ocmanager.repository.CreateRepositoryRequest": { + "type": "object", + "properties": { + "spec": { + "title": "spec", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositorySpec" + } + }, + "title": "trpc.ocmanager.repository.CreateRepositoryRequest", + "description": "CreateRepositoryRequest" + }, + "trpc.ocmanager.repository.CreateRepositoryStorageNodeRequest": { + "type": "object", + "properties": { + "spec": { + "title": "spec", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryStorageNodeSpec" + } + }, + "title": "trpc.ocmanager.repository.CreateRepositoryStorageNodeRequest", + "description": "CreateRepositoryStorageNodeRequest" + }, + "trpc.ocmanager.repository.CreateRepositoryUpstreamProfileRequest": { + "type": "object", + "properties": { + "spec": { + "title": "spec", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryUpstreamProfileSpec" + } + }, + "title": "trpc.ocmanager.repository.CreateRepositoryUpstreamProfileRequest", + "description": "CreateRepositoryUpstreamProfileRequest" + }, + "trpc.ocmanager.repository.DeleteRepositoryGPGKeyRequest": { + "type": "object", + "properties": { + "confirm_delete": { + "title": "confirm_delete", + "type": "boolean" + }, + "key_id": { + "title": "key_id", + "type": "string" + }, + "repository_id": { + "title": "repository_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.DeleteRepositoryGPGKeyRequest", + "description": "DeleteRepositoryGPGKeyRequest" + }, + "trpc.ocmanager.repository.DeleteRepositoryPackageRequest": { + "type": "object", + "properties": { + "nevra": { + "title": "nevra", + "type": "string" + }, + "repository_id": { + "title": "repository_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.DeleteRepositoryPackageRequest", + "description": "DeleteRepositoryPackageRequest" + }, + "trpc.ocmanager.repository.DeleteRepositoryRequest": { + "type": "object", + "properties": { + "confirm_delete": { + "title": "confirm_delete", + "type": "boolean" + }, + "repository_id": { + "title": "repository_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.DeleteRepositoryRequest", + "description": "DeleteRepositoryRequest" + }, + "trpc.ocmanager.repository.DeleteRepositoryStorageNodeRequest": { + "type": "object", + "properties": { + "confirm_delete": { + "title": "confirm_delete", + "type": "boolean" + }, + "storage_node_id": { + "title": "storage_node_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.DeleteRepositoryStorageNodeRequest", + "description": "DeleteRepositoryStorageNodeRequest" + }, + "trpc.ocmanager.repository.DeleteRepositoryUpstreamProfileRequest": { + "type": "object", + "properties": { + "upstream_profile_id": { + "title": "upstream_profile_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.DeleteRepositoryUpstreamProfileRequest", + "description": "DeleteRepositoryUpstreamProfileRequest" + }, + "trpc.ocmanager.repository.DescribeRepositoryDistributionRequest": { + "type": "object", + "properties": { + "distribution_id": { + "title": "distribution_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.DescribeRepositoryDistributionRequest", + "description": "DescribeRepositoryDistributionRequest" + }, + "trpc.ocmanager.repository.DescribeRepositoryOCAIAnalysisRequest": { + "type": "object", + "properties": { + "analysis_id": { + "title": "analysis_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.DescribeRepositoryOCAIAnalysisRequest", + "description": "DescribeRepositoryOCAIAnalysisRequest" + }, + "trpc.ocmanager.repository.DescribeRepositoryPackageInstallRequest": { + "type": "object", + "properties": { + "install_id": { + "title": "install_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.DescribeRepositoryPackageInstallRequest", + "description": "DescribeRepositoryPackageInstallRequest" + }, + "trpc.ocmanager.repository.DescribeRepositoryRequest": { + "type": "object", + "properties": { + "repository_id": { + "title": "repository_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.DescribeRepositoryRequest", + "description": "DescribeRepositoryRequest" + }, + "trpc.ocmanager.repository.DescribeRepositoryStorageNodeRequest": { + "type": "object", + "properties": { + "storage_node_id": { + "title": "storage_node_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.DescribeRepositoryStorageNodeRequest", + "description": "DescribeRepositoryStorageNodeRequest" + }, + "trpc.ocmanager.repository.DescribeRepositorySyncScheduleRequest": { + "type": "object", + "properties": { + "repository_id": { + "title": "repository_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.DescribeRepositorySyncScheduleRequest", + "description": "DescribeRepositorySyncScheduleRequest" + }, + "trpc.ocmanager.repository.DescribeRepositoryTaskLogRequest": { + "type": "object", + "properties": { + "task_id": { + "title": "task_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.DescribeRepositoryTaskLogRequest", + "description": "DescribeRepositoryTaskLogRequest" + }, + "trpc.ocmanager.repository.DescribeRepositoryTaskRequest": { + "type": "object", + "properties": { + "task_id": { + "title": "task_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.DescribeRepositoryTaskRequest", + "description": "DescribeRepositoryTaskRequest" + }, + "trpc.ocmanager.repository.DescribeRepositoryUploadChunksResponse": { + "type": "object", + "properties": { + "chunks": { + "title": "chunks", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryUploadChunk" + } + }, + "missing_chunk_indexes": { + "title": "missing_chunk_indexes", + "type": "array", + "format": "int32", + "items": { + "type": "integer", + "format": "int32" + } + }, + "upload": { + "title": "upload", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryUpload" + } + }, + "title": "trpc.ocmanager.repository.DescribeRepositoryUploadChunksResponse", + "description": "DescribeRepositoryUploadChunksResponse" + }, + "trpc.ocmanager.repository.DescribeRepositoryUploadRequest": { + "type": "object", + "properties": { + "upload_id": { + "title": "upload_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.DescribeRepositoryUploadRequest", + "description": "DescribeRepositoryUploadRequest" + }, + "trpc.ocmanager.repository.DescribeRepositoryUpstreamProfileRequest": { + "type": "object", + "properties": { + "upstream_profile_id": { + "title": "upstream_profile_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.DescribeRepositoryUpstreamProfileRequest", + "description": "DescribeRepositoryUpstreamProfileRequest" + }, + "trpc.ocmanager.repository.DownloadRepositoryNodeArtifactRequest": { + "type": "object", + "properties": { + "artifact_id": { + "title": "artifact_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.DownloadRepositoryNodeArtifactRequest", + "description": "DownloadRepositoryNodeArtifactRequest" + }, + "trpc.ocmanager.repository.DownloadRepositoryTaskLogRequest": { + "type": "object", + "properties": { + "task_id": { + "title": "task_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.DownloadRepositoryTaskLogRequest", + "description": "DownloadRepositoryTaskLogRequest" + }, + "trpc.ocmanager.repository.ExportRepositoryDistributionResultRequest": { + "type": "object", + "properties": { + "distribution_id": { + "title": "distribution_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.ExportRepositoryDistributionResultRequest", + "description": "ExportRepositoryDistributionResultRequest" + }, + "trpc.ocmanager.repository.ExportRepositoryInventoryRequest": { + "type": "object", + "properties": { + "project_id": { + "title": "project_id", + "type": "string", + "format": "int64" + } + }, + "title": "trpc.ocmanager.repository.ExportRepositoryInventoryRequest", + "description": "ExportRepositoryInventoryRequest" + }, + "trpc.ocmanager.repository.ExportRepositoryTaskDiagnosisRequest": { + "type": "object", + "properties": { + "task_id": { + "title": "task_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.ExportRepositoryTaskDiagnosisRequest", + "description": "ExportRepositoryTaskDiagnosisRequest" + }, + "trpc.ocmanager.repository.ExportRepositoryValidationReportRequest": { + "type": "object", + "properties": { + "repository_id": { + "title": "repository_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.ExportRepositoryValidationReportRequest", + "description": "ExportRepositoryValidationReportRequest" + }, + "trpc.ocmanager.repository.GenerateRepositoryConfigRequest": { + "type": "object", + "properties": { + "immutable_revision": { + "title": "immutable_revision", + "type": "boolean" + }, + "repository_id": { + "title": "repository_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.GenerateRepositoryConfigRequest", + "description": "GenerateRepositoryConfigRequest" + }, + "trpc.ocmanager.repository.ListRepositoriesRequest": { + "type": "object", + "properties": { + "architecture": { + "title": "architecture", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_ARCHITECTURE_UNSPECIFIED - \n * 1 - REPOSITORY_ARCHITECTURE_X86_64 - \n * 2 - REPOSITORY_ARCHITECTURE_AARCH64 - \n * 3 - REPOSITORY_ARCHITECTURE_NOARCH - \n", + "enum": [ + 0, + 1, + 2, + 3 + ] + }, + "keyword": { + "title": "keyword", + "type": "string" + }, + "page_request": { + "title": "page_request", + "$ref": "#/definitions/trpc.ocmanager.common.PageRequest" + }, + "product_version": { + "title": "product_version", + "type": "string" + }, + "project_id": { + "title": "project_id", + "type": "string", + "format": "int64" + }, + "system_product": { + "title": "system_product", + "type": "string" + }, + "type": { + "title": "type", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_TYPE_UNSPECIFIED - \n * 1 - REPOSITORY_TYPE_BASELINE - \n * 2 - REPOSITORY_TYPE_UPDATE - \n * 3 - REPOSITORY_TYPE_PRIVATE - \n", + "enum": [ + 0, + 1, + 2, + 3 + ] + } + }, + "title": "trpc.ocmanager.repository.ListRepositoriesRequest", + "description": "ListRepositoriesRequest" + }, + "trpc.ocmanager.repository.ListRepositoriesResponse": { + "type": "object", + "properties": { + "page_response": { + "title": "page_response", + "$ref": "#/definitions/trpc.ocmanager.common.PageResponse" + }, + "repositories": { + "title": "repositories", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.Repository" + } + } + }, + "title": "trpc.ocmanager.repository.ListRepositoriesResponse", + "description": "ListRepositoriesResponse" + }, + "trpc.ocmanager.repository.ListRepositoryDistributionNodesRequest": { + "type": "object", + "title": "trpc.ocmanager.repository.ListRepositoryDistributionNodesRequest", + "description": "ListRepositoryDistributionNodesRequest" + }, + "trpc.ocmanager.repository.ListRepositoryDistributionNodesResponse": { + "type": "object", + "properties": { + "nodes": { + "title": "nodes", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryDistributionNode" + } + } + }, + "title": "trpc.ocmanager.repository.ListRepositoryDistributionNodesResponse", + "description": "ListRepositoryDistributionNodesResponse" + }, + "trpc.ocmanager.repository.ListRepositoryDistributionsRequest": { + "type": "object", + "properties": { + "page_request": { + "title": "page_request", + "$ref": "#/definitions/trpc.ocmanager.common.PageRequest" + }, + "repository_id": { + "title": "repository_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.ListRepositoryDistributionsRequest", + "description": "ListRepositoryDistributionsRequest" + }, + "trpc.ocmanager.repository.ListRepositoryDistributionsResponse": { + "type": "object", + "properties": { + "distributions": { + "title": "distributions", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryDistribution" + } + }, + "page_response": { + "title": "page_response", + "$ref": "#/definitions/trpc.ocmanager.common.PageResponse" + } + }, + "title": "trpc.ocmanager.repository.ListRepositoryDistributionsResponse", + "description": "ListRepositoryDistributionsResponse" + }, + "trpc.ocmanager.repository.ListRepositoryGPGKeysRequest": { + "type": "object", + "properties": { + "repository_id": { + "title": "repository_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.ListRepositoryGPGKeysRequest", + "description": "ListRepositoryGPGKeysRequest" + }, + "trpc.ocmanager.repository.ListRepositoryGPGKeysResponse": { + "type": "object", + "properties": { + "keys": { + "title": "keys", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryGPGKey" + } + } + }, + "title": "trpc.ocmanager.repository.ListRepositoryGPGKeysResponse", + "description": "ListRepositoryGPGKeysResponse" + }, + "trpc.ocmanager.repository.ListRepositoryNodeArtifactsRequest": { + "type": "object", + "properties": { + "project_id": { + "title": "project_id", + "type": "string", + "format": "int64" + }, + "status": { + "title": "status", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.ListRepositoryNodeArtifactsRequest", + "description": "ListRepositoryNodeArtifactsRequest" + }, + "trpc.ocmanager.repository.ListRepositoryNodeArtifactsResponse": { + "type": "object", + "properties": { + "artifacts": { + "title": "artifacts", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryNodeArtifact" + } + } + }, + "title": "trpc.ocmanager.repository.ListRepositoryNodeArtifactsResponse", + "description": "ListRepositoryNodeArtifactsResponse" + }, + "trpc.ocmanager.repository.ListRepositoryNodeInstallSourcesRequest": { + "type": "object", + "properties": { + "project_id": { + "title": "project_id", + "type": "string", + "format": "int64" + } + }, + "title": "trpc.ocmanager.repository.ListRepositoryNodeInstallSourcesRequest", + "description": "ListRepositoryNodeInstallSourcesRequest" + }, + "trpc.ocmanager.repository.ListRepositoryNodeInstallSourcesResponse": { + "type": "object", + "properties": { + "install_sources": { + "title": "install_sources", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryNodeInstallSource" + } + } + }, + "title": "trpc.ocmanager.repository.ListRepositoryNodeInstallSourcesResponse", + "description": "ListRepositoryNodeInstallSourcesResponse" + }, + "trpc.ocmanager.repository.ListRepositoryNodeRegistrationAllowlistsRequest": { + "type": "object", + "properties": { + "project_id": { + "title": "project_id", + "type": "string", + "format": "int64" + } + }, + "title": "trpc.ocmanager.repository.ListRepositoryNodeRegistrationAllowlistsRequest", + "description": "ListRepositoryNodeRegistrationAllowlistsRequest" + }, + "trpc.ocmanager.repository.ListRepositoryNodeRegistrationAllowlistsResponse": { + "type": "object", + "properties": { + "allowlists": { + "title": "allowlists", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryNodeRegistrationAllowlist" + } + } + }, + "title": "trpc.ocmanager.repository.ListRepositoryNodeRegistrationAllowlistsResponse", + "description": "ListRepositoryNodeRegistrationAllowlistsResponse" + }, + "trpc.ocmanager.repository.ListRepositoryPackagesRequest": { + "type": "object", + "properties": { + "architecture": { + "title": "architecture", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_ARCHITECTURE_UNSPECIFIED - \n * 1 - REPOSITORY_ARCHITECTURE_X86_64 - \n * 2 - REPOSITORY_ARCHITECTURE_AARCH64 - \n * 3 - REPOSITORY_ARCHITECTURE_NOARCH - \n", + "enum": [ + 0, + 1, + 2, + 3 + ] + }, + "name": { + "title": "name", + "type": "string" + }, + "page_request": { + "title": "page_request", + "$ref": "#/definitions/trpc.ocmanager.common.PageRequest" + }, + "repository_id": { + "title": "repository_id", + "type": "string" + }, + "revision_id": { + "title": "revision_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.ListRepositoryPackagesRequest", + "description": "ListRepositoryPackagesRequest" + }, + "trpc.ocmanager.repository.ListRepositoryPackagesResponse": { + "type": "object", + "properties": { + "packages": { + "title": "packages", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryPackage" + } + }, + "page_response": { + "title": "page_response", + "$ref": "#/definitions/trpc.ocmanager.common.PageResponse" + }, + "revision_id": { + "title": "revision_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.ListRepositoryPackagesResponse", + "description": "ListRepositoryPackagesResponse" + }, + "trpc.ocmanager.repository.ListRepositoryPreflightRecordsRequest": { + "type": "object", + "properties": { + "page_request": { + "title": "page_request", + "$ref": "#/definitions/trpc.ocmanager.common.PageRequest" + }, + "repository_id": { + "title": "repository_id", + "type": "string" + }, + "storage_node_id": { + "title": "storage_node_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.ListRepositoryPreflightRecordsRequest", + "description": "ListRepositoryPreflightRecordsRequest" + }, + "trpc.ocmanager.repository.ListRepositoryPreflightRecordsResponse": { + "type": "object", + "properties": { + "page_response": { + "title": "page_response", + "$ref": "#/definitions/trpc.ocmanager.common.PageResponse" + }, + "records": { + "title": "records", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryPreflightRecord" + } + } + }, + "title": "trpc.ocmanager.repository.ListRepositoryPreflightRecordsResponse", + "description": "ListRepositoryPreflightRecordsResponse" + }, + "trpc.ocmanager.repository.ListRepositoryQuarantineRequest": { + "type": "object", + "properties": { + "page_request": { + "title": "page_request", + "$ref": "#/definitions/trpc.ocmanager.common.PageRequest" + }, + "reason_code": { + "title": "reason_code", + "type": "string" + }, + "repository_id": { + "title": "repository_id", + "type": "string" + }, + "task_id": { + "title": "task_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.ListRepositoryQuarantineRequest", + "description": "ListRepositoryQuarantineRequest" + }, + "trpc.ocmanager.repository.ListRepositoryQuarantineResponse": { + "type": "object", + "properties": { + "items": { + "title": "items", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryQuarantineItem" + } + }, + "page_response": { + "title": "page_response", + "$ref": "#/definitions/trpc.ocmanager.common.PageResponse" + } + }, + "title": "trpc.ocmanager.repository.ListRepositoryQuarantineResponse", + "description": "ListRepositoryQuarantineResponse" + }, + "trpc.ocmanager.repository.ListRepositoryStorageNodesRequest": { + "type": "object", + "properties": { + "keyword": { + "title": "keyword", + "type": "string" + }, + "page_request": { + "title": "page_request", + "$ref": "#/definitions/trpc.ocmanager.common.PageRequest" + }, + "project_id": { + "title": "project_id", + "type": "string", + "format": "int64" + }, + "status": { + "title": "status", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_STORAGE_NODE_STATUS_UNSPECIFIED - \n * 1 - REPOSITORY_STORAGE_NODE_STATUS_UNKNOWN - \n * 2 - REPOSITORY_STORAGE_NODE_STATUS_ONLINE - \n * 3 - REPOSITORY_STORAGE_NODE_STATUS_OFFLINE - \n * 4 - REPOSITORY_STORAGE_NODE_STATUS_DISABLED - \n * 5 - REPOSITORY_STORAGE_NODE_STATUS_PENDING - \n * 6 - REPOSITORY_STORAGE_NODE_STATUS_INSTALLING - \n * 7 - REPOSITORY_STORAGE_NODE_STATUS_INSTALL_FAILED - \n", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ] + } + }, + "title": "trpc.ocmanager.repository.ListRepositoryStorageNodesRequest", + "description": "ListRepositoryStorageNodesRequest" + }, + "trpc.ocmanager.repository.ListRepositoryStorageNodesResponse": { + "type": "object", + "properties": { + "critical_nodes": { + "title": "critical_nodes", + "type": "string", + "format": "int64" + }, + "online_nodes": { + "title": "online_nodes", + "type": "string", + "format": "int64" + }, + "page_response": { + "title": "page_response", + "$ref": "#/definitions/trpc.ocmanager.common.PageResponse" + }, + "storage_nodes": { + "title": "storage_nodes", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryStorageNode" + } + }, + "total_capacity_bytes": { + "title": "total_capacity_bytes", + "type": "string", + "format": "int64" + }, + "total_nodes": { + "title": "total_nodes", + "type": "string", + "format": "int64" + }, + "used_capacity_bytes": { + "title": "used_capacity_bytes", + "type": "string", + "format": "int64" + } + }, + "title": "trpc.ocmanager.repository.ListRepositoryStorageNodesResponse", + "description": "ListRepositoryStorageNodesResponse" + }, + "trpc.ocmanager.repository.ListRepositoryTasksRequest": { + "type": "object", + "properties": { + "created_at_from": { + "title": "created_at_from", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "created_at_to": { + "title": "created_at_to", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "page_request": { + "title": "page_request", + "$ref": "#/definitions/trpc.ocmanager.common.PageRequest" + }, + "project_id": { + "title": "project_id", + "type": "string", + "format": "int64" + }, + "repository_id": { + "title": "repository_id", + "type": "string" + }, + "status": { + "title": "status", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_TASK_STATUS_UNSPECIFIED - \n * 1 - REPOSITORY_TASK_STATUS_PENDING - \n * 2 - REPOSITORY_TASK_STATUS_RUNNING - \n * 3 - REPOSITORY_TASK_STATUS_CANCEL_REQUESTED - \n * 4 - REPOSITORY_TASK_STATUS_CANCELED - \n * 5 - REPOSITORY_TASK_STATUS_SUCCESS - \n * 6 - REPOSITORY_TASK_STATUS_FAILED - \n * 7 - REPOSITORY_TASK_STATUS_PARTIAL_SUCCESS - \n", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ] + }, + "storage_node_id": { + "title": "storage_node_id", + "type": "string" + }, + "trigger": { + "title": "trigger", + "type": "string" + }, + "type": { + "title": "type", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_TASK_TYPE_UNSPECIFIED - \n * 1 - REPOSITORY_TASK_TYPE_SYNC - \n * 2 - REPOSITORY_TASK_TYPE_UPLOAD - \n * 3 - REPOSITORY_TASK_TYPE_DELETE_PACKAGE - \n * 4 - REPOSITORY_TASK_TYPE_REBUILD_METADATA - \n * 5 - REPOSITORY_TASK_TYPE_DELETE_REPOSITORY - \n * 6 - REPOSITORY_TASK_TYPE_PUBLISH - \n * 7 - REPOSITORY_TASK_TYPE_VALIDATE - \n * 8 - REPOSITORY_TASK_TYPE_DISTRIBUTE - \n * 9 - REPOSITORY_TASK_TYPE_MIGRATE_REPOSITORY - \n * 10 - REPOSITORY_TASK_TYPE_NODE_INSTALL - \n * 11 - REPOSITORY_TASK_TYPE_PACKAGE_INSTALL - \n * 12 - REPOSITORY_TASK_TYPE_INVENTORY_IMPORT - \n", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12 + ] + } + }, + "title": "trpc.ocmanager.repository.ListRepositoryTasksRequest", + "description": "ListRepositoryTasksRequest" + }, + "trpc.ocmanager.repository.ListRepositoryTasksResponse": { + "type": "object", + "properties": { + "page_response": { + "title": "page_response", + "$ref": "#/definitions/trpc.ocmanager.common.PageResponse" + }, + "statistics": { + "title": "statistics", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryTaskStatistics" + }, + "tasks": { + "title": "tasks", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryTask" + } + } + }, + "title": "trpc.ocmanager.repository.ListRepositoryTasksResponse", + "description": "ListRepositoryTasksResponse" + }, + "trpc.ocmanager.repository.ListRepositoryUpstreamProfilesRequest": { + "type": "object", + "properties": { + "enabled": { + "title": "enabled", + "type": "boolean" + }, + "keyword": { + "title": "keyword", + "type": "string" + }, + "page_request": { + "title": "page_request", + "$ref": "#/definitions/trpc.ocmanager.common.PageRequest" + }, + "project_id": { + "title": "project_id", + "type": "string", + "format": "int64" + }, + "system_product": { + "title": "system_product", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.ListRepositoryUpstreamProfilesRequest", + "description": "ListRepositoryUpstreamProfilesRequest" + }, + "trpc.ocmanager.repository.ListRepositoryUpstreamProfilesResponse": { + "type": "object", + "properties": { + "page_response": { + "title": "page_response", + "$ref": "#/definitions/trpc.ocmanager.common.PageResponse" + }, + "profiles": { + "title": "profiles", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryUpstreamProfile" + } + } + }, + "title": "trpc.ocmanager.repository.ListRepositoryUpstreamProfilesResponse", + "description": "ListRepositoryUpstreamProfilesResponse" + }, + "trpc.ocmanager.repository.MigrateRepositoryRequest": { + "type": "object", + "properties": { + "repository_id": { + "title": "repository_id", + "type": "string" + }, + "target_storage_node_id": { + "title": "target_storage_node_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.MigrateRepositoryRequest", + "description": "Kept at the end so adding the migration API does not renumber existing\n generated message indexes." + }, + "trpc.ocmanager.repository.PreviewRepositoryInventoryImportRequest": { + "type": "object", + "properties": { + "conflict_strategy": { + "title": "conflict_strategy", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_INVENTORY_CONFLICT_STRATEGY_UNSPECIFIED - \n * 1 - REPOSITORY_INVENTORY_CONFLICT_STRATEGY_FAIL - \n * 2 - REPOSITORY_INVENTORY_CONFLICT_STRATEGY_SKIP - \n * 3 - REPOSITORY_INVENTORY_CONFLICT_STRATEGY_RENAME - \n", + "enum": [ + 0, + 1, + 2, + 3 + ] + }, + "content": { + "title": "content", + "type": "string" + }, + "default_storage_node_id": { + "title": "default_storage_node_id", + "type": "string" + }, + "project_id": { + "title": "project_id", + "type": "string", + "format": "int64" + }, + "signing_key_mappings": { + "title": "signing_key_mappings", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryInventorySigningKeyMapping" + } + }, + "storage_node_mappings": { + "title": "storage_node_mappings", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryInventoryStorageNodeMapping" + } + } + }, + "title": "trpc.ocmanager.repository.PreviewRepositoryInventoryImportRequest", + "description": "PreviewRepositoryInventoryImportRequest" + }, + "trpc.ocmanager.repository.PreviewRepositoryPackageDeletionRequest": { + "type": "object", + "properties": { + "nevras": { + "title": "nevras", + "type": "array", + "items": { + "type": "string" + } + }, + "repository_id": { + "title": "repository_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.PreviewRepositoryPackageDeletionRequest", + "description": "PreviewRepositoryPackageDeletionRequest" + }, + "trpc.ocmanager.repository.PreviewRepositoryPackageInstallRequest": { + "type": "object", + "properties": { + "package_names": { + "title": "package_names", + "type": "array", + "items": { + "type": "string" + } + }, + "repository_id": { + "title": "repository_id", + "type": "string" + }, + "revision_id": { + "title": "revision_id", + "type": "string" + }, + "target_agent_ids": { + "title": "target_agent_ids", + "type": "array", + "items": { + "type": "string" + } + }, + "timeout_seconds": { + "title": "timeout_seconds", + "type": "integer", + "format": "int32" + } + }, + "title": "trpc.ocmanager.repository.PreviewRepositoryPackageInstallRequest", + "description": "PreviewRepositoryPackageInstallRequest" + }, + "trpc.ocmanager.repository.ProbeRepositoryStorageNodeRequest": { + "type": "object", + "properties": { + "storage_node_id": { + "title": "storage_node_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.ProbeRepositoryStorageNodeRequest", + "description": "ProbeRepositoryStorageNodeRequest" + }, + "trpc.ocmanager.repository.PutRepositoryUploadChunkRequest": { + "type": "object", + "properties": { + "chunk_index": { + "title": "chunk_index", + "type": "integer", + "format": "int32" + }, + "chunk_sha256": { + "title": "chunk_sha256", + "type": "string" + }, + "data": { + "title": "data", + "type": "string", + "format": "bytes" + }, + "upload_id": { + "title": "upload_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.PutRepositoryUploadChunkRequest", + "description": "PutRepositoryUploadChunkRequest" + }, + "trpc.ocmanager.repository.PutRepositoryUploadChunkResponse": { + "type": "object", + "properties": { + "chunk": { + "title": "chunk", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryUploadChunk" + }, + "upload": { + "title": "upload", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryUpload" + } + }, + "title": "trpc.ocmanager.repository.PutRepositoryUploadChunkResponse", + "description": "PutRepositoryUploadChunkResponse" + }, + "trpc.ocmanager.repository.RegisterRepositoryGPGKeyRequest": { + "type": "object", + "properties": { + "public_key": { + "title": "public_key", + "type": "string", + "format": "bytes" + }, + "repository_id": { + "title": "repository_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RegisterRepositoryGPGKeyRequest", + "description": "RegisterRepositoryGPGKeyRequest" + }, + "trpc.ocmanager.repository.RegisterRepositoryNodeArtifactRequest": { + "type": "object", + "properties": { + "spec": { + "title": "spec", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryNodeArtifactSpec" + } + }, + "title": "trpc.ocmanager.repository.RegisterRepositoryNodeArtifactRequest", + "description": "RegisterRepositoryNodeArtifactRequest" + }, + "trpc.ocmanager.repository.Repository": { + "type": "object", + "properties": { + "active_revision_id": { + "title": "active_revision_id", + "type": "string" + }, + "allow_upload": { + "title": "allow_upload", + "type": "boolean" + }, + "architecture": { + "title": "architecture", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_ARCHITECTURE_UNSPECIFIED - \n * 1 - REPOSITORY_ARCHITECTURE_X86_64 - \n * 2 - REPOSITORY_ARCHITECTURE_AARCH64 - \n * 3 - REPOSITORY_ARCHITECTURE_NOARCH - \n", + "enum": [ + 0, + 1, + 2, + 3 + ] + }, + "bandwidth_limit_bytes": { + "title": "bandwidth_limit_bytes", + "type": "string", + "format": "int64" + }, + "created_at": { + "title": "created_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "creator": { + "title": "creator", + "type": "string" + }, + "description": { + "title": "description", + "type": "string" + }, + "enabled": { + "title": "enabled", + "type": "boolean" + }, + "exclude_packages": { + "title": "exclude_packages", + "type": "array", + "items": { + "type": "string" + } + }, + "gpg_policy": { + "title": "gpg_policy", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_GPG_POLICY_UNSPECIFIED - \n * 1 - REPOSITORY_GPG_POLICY_DISABLED - \n * 2 - REPOSITORY_GPG_POLICY_VERIFY_PACKAGE - \n * 3 - REPOSITORY_GPG_POLICY_AUDIT - \n * 4 - REPOSITORY_GPG_POLICY_ENFORCE - \n", + "enum": [ + 0, + 1, + 2, + 3, + 4 + ] + }, + "id": { + "title": "id", + "type": "string" + }, + "include_packages": { + "title": "include_packages", + "type": "array", + "items": { + "type": "string" + } + }, + "last_sync_at": { + "title": "last_sync_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "last_sync_status": { + "title": "last_sync_status", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_TASK_STATUS_UNSPECIFIED - \n * 1 - REPOSITORY_TASK_STATUS_PENDING - \n * 2 - REPOSITORY_TASK_STATUS_RUNNING - \n * 3 - REPOSITORY_TASK_STATUS_CANCEL_REQUESTED - \n * 4 - REPOSITORY_TASK_STATUS_CANCELED - \n * 5 - REPOSITORY_TASK_STATUS_SUCCESS - \n * 6 - REPOSITORY_TASK_STATUS_FAILED - \n * 7 - REPOSITORY_TASK_STATUS_PARTIAL_SUCCESS - \n", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ] + }, + "logical_bytes": { + "title": "logical_bytes", + "type": "string", + "format": "int64" + }, + "max_parallel_downloads": { + "title": "max_parallel_downloads", + "type": "integer", + "format": "int32" + }, + "metadata_signing_key_ref": { + "title": "metadata_signing_key_ref", + "type": "string" + }, + "name": { + "title": "name", + "type": "string" + }, + "package_count": { + "title": "package_count", + "type": "string", + "format": "int64" + }, + "primary_storage_node_id": { + "title": "primary_storage_node_id", + "type": "string" + }, + "product_version": { + "title": "product_version", + "type": "string" + }, + "project_id": { + "title": "project_id", + "type": "string", + "format": "int64" + }, + "publish_base_url_snapshot": { + "title": "publish_base_url_snapshot", + "type": "string" + }, + "publish_key": { + "title": "publish_key", + "type": "string" + }, + "revision_count": { + "title": "revision_count", + "type": "string", + "format": "int64" + }, + "status": { + "title": "status", + "type": "string" + }, + "storage_location": { + "title": "storage_location", + "type": "string" + }, + "sync_mode": { + "title": "sync_mode", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_SYNC_MODE_UNSPECIFIED - \n * 1 - REPOSITORY_SYNC_MODE_MIRROR - \n * 2 - REPOSITORY_SYNC_MODE_APPEND - \n", + "enum": [ + 0, + 1, + 2 + ] + }, + "system_product": { + "title": "system_product", + "type": "string" + }, + "type": { + "title": "type", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_TYPE_UNSPECIFIED - \n * 1 - REPOSITORY_TYPE_BASELINE - \n * 2 - REPOSITORY_TYPE_UPDATE - \n * 3 - REPOSITORY_TYPE_PRIVATE - \n", + "enum": [ + 0, + 1, + 2, + 3 + ] + }, + "updated_at": { + "title": "updated_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "upstream_url": { + "title": "upstream_url", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.Repository", + "description": "Repository" + }, + "trpc.ocmanager.repository.RepositoryConfig": { + "type": "object", + "properties": { + "active_revision_id": { + "title": "active_revision_id", + "type": "string" + }, + "baseurl": { + "title": "baseurl", + "type": "string" + }, + "content": { + "title": "content", + "type": "string" + }, + "content_type": { + "title": "content_type", + "type": "string" + }, + "enabled": { + "title": "enabled", + "type": "boolean" + }, + "file_name": { + "title": "file_name", + "type": "string" + }, + "gpgcheck": { + "title": "gpgcheck", + "type": "boolean" + }, + "gpgkey_urls": { + "title": "gpgkey_urls", + "type": "array", + "items": { + "type": "string" + } + }, + "immutable_revision": { + "title": "immutable_revision", + "type": "boolean" + }, + "repo_gpgcheck": { + "title": "repo_gpgcheck", + "type": "boolean" + }, + "repo_id": { + "title": "repo_id", + "type": "string" + }, + "repository_id": { + "title": "repository_id", + "type": "string" + }, + "revision_url": { + "title": "revision_url", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryConfig", + "description": "RepositoryConfig" + }, + "trpc.ocmanager.repository.RepositoryConfigResponse": { + "type": "object", + "properties": { + "config": { + "title": "config", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryConfig" + } + }, + "title": "trpc.ocmanager.repository.RepositoryConfigResponse", + "description": "RepositoryConfigResponse" + }, + "trpc.ocmanager.repository.RepositoryDiagnosis": { + "type": "object", + "properties": { + "error_code": { + "title": "error_code", + "type": "string" + }, + "evidence": { + "title": "evidence", + "type": "array", + "items": { + "type": "string" + } + }, + "failed_step": { + "title": "failed_step", + "type": "string" + }, + "failure_category": { + "title": "failure_category", + "type": "string" + }, + "generated_at": { + "title": "generated_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "likely_cause": { + "title": "likely_cause", + "type": "string" + }, + "recommended_action": { + "title": "recommended_action", + "type": "string" + }, + "rule_id": { + "title": "rule_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryDiagnosis", + "description": "RepositoryDiagnosis" + }, + "trpc.ocmanager.repository.RepositoryDiagnosisReportResponse": { + "type": "object", + "properties": { + "content": { + "title": "content", + "type": "string" + }, + "content_type": { + "title": "content_type", + "type": "string" + }, + "file_name": { + "title": "file_name", + "type": "string" + }, + "sha256": { + "title": "sha256", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryDiagnosisReportResponse", + "description": "RepositoryDiagnosisReportResponse" + }, + "trpc.ocmanager.repository.RepositoryDistribution": { + "type": "object", + "properties": { + "artifact_md5": { + "title": "artifact_md5", + "type": "string" + }, + "created_at": { + "title": "created_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "error_code": { + "title": "error_code", + "type": "string" + }, + "error_message": { + "title": "error_message", + "type": "string" + }, + "finished_at": { + "title": "finished_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "id": { + "title": "id", + "type": "string" + }, + "manifest_sha256": { + "title": "manifest_sha256", + "type": "string" + }, + "project_id": { + "title": "project_id", + "type": "string", + "format": "int64" + }, + "repomd_sha256": { + "title": "repomd_sha256", + "type": "string" + }, + "repository_id": { + "title": "repository_id", + "type": "string" + }, + "repository_task_id": { + "title": "repository_task_id", + "type": "string" + }, + "requested_by": { + "title": "requested_by", + "type": "string" + }, + "revision_id": { + "title": "revision_id", + "type": "string" + }, + "started_at": { + "title": "started_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "status": { + "title": "status", + "type": "string" + }, + "targets": { + "title": "targets", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryDistributionTarget" + } + }, + "timeout_seconds": { + "title": "timeout_seconds", + "type": "integer", + "format": "int32" + }, + "tms_task_id": { + "title": "tms_task_id", + "type": "string" + }, + "updated_at": { + "title": "updated_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + } + }, + "title": "trpc.ocmanager.repository.RepositoryDistribution", + "description": "RepositoryDistribution" + }, + "trpc.ocmanager.repository.RepositoryDistributionNode": { + "type": "object", + "properties": { + "agent_id": { + "title": "agent_id", + "type": "string" + }, + "asset_status": { + "title": "asset_status", + "type": "string" + }, + "connected": { + "title": "connected", + "type": "boolean" + }, + "ip": { + "title": "ip", + "type": "string" + }, + "last_heartbeat_at": { + "title": "last_heartbeat_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + } + }, + "title": "trpc.ocmanager.repository.RepositoryDistributionNode", + "description": "RepositoryDistributionNode" + }, + "trpc.ocmanager.repository.RepositoryDistributionResponse": { + "type": "object", + "properties": { + "distribution": { + "title": "distribution", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryDistribution" + }, + "task": { + "title": "task", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryTask" + } + }, + "title": "trpc.ocmanager.repository.RepositoryDistributionResponse", + "description": "RepositoryDistributionResponse" + }, + "trpc.ocmanager.repository.RepositoryDistributionTarget": { + "type": "object", + "properties": { + "agent_id": { + "title": "agent_id", + "type": "string" + }, + "attempt_count": { + "title": "attempt_count", + "type": "integer", + "format": "int32" + }, + "error_code": { + "title": "error_code", + "type": "string" + }, + "error_message": { + "title": "error_message", + "type": "string" + }, + "exit_code": { + "title": "exit_code", + "type": "integer", + "format": "int32" + }, + "finished_at": { + "title": "finished_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "id": { + "title": "id", + "type": "string" + }, + "output": { + "title": "output", + "type": "string" + }, + "output_ref": { + "title": "output_ref", + "type": "string" + }, + "output_truncated": { + "title": "output_truncated", + "type": "boolean" + }, + "started_at": { + "title": "started_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "status": { + "title": "status", + "type": "string" + }, + "target_ip": { + "title": "target_ip", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryDistributionTarget", + "description": "RepositoryDistributionTarget" + }, + "trpc.ocmanager.repository.RepositoryExportResponse": { + "type": "object", + "properties": { + "content": { + "title": "content", + "type": "string" + }, + "content_type": { + "title": "content_type", + "type": "string" + }, + "file_name": { + "title": "file_name", + "type": "string" + }, + "sha256": { + "title": "sha256", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryExportResponse", + "description": "RepositoryExportResponse" + }, + "trpc.ocmanager.repository.RepositoryGPGKey": { + "type": "object", + "properties": { + "created_at": { + "title": "created_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "fingerprint": { + "title": "fingerprint", + "type": "string" + }, + "id": { + "title": "id", + "type": "string" + }, + "key_id": { + "title": "key_id", + "type": "string" + }, + "public_key_path": { + "title": "public_key_path", + "type": "string" + }, + "public_key_url": { + "title": "public_key_url", + "type": "string" + }, + "repository_id": { + "title": "repository_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryGPGKey", + "description": "RepositoryGPGKey" + }, + "trpc.ocmanager.repository.RepositoryGPGKeyResponse": { + "type": "object", + "properties": { + "key": { + "title": "key", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryGPGKey" + } + }, + "title": "trpc.ocmanager.repository.RepositoryGPGKeyResponse", + "description": "RepositoryGPGKeyResponse" + }, + "trpc.ocmanager.repository.RepositoryInventoryImportItem": { + "type": "object", + "properties": { + "action": { + "title": "action", + "type": "string" + }, + "message": { + "title": "message", + "type": "string" + }, + "source_name": { + "title": "source_name", + "type": "string" + }, + "source_repository_id": { + "title": "source_repository_id", + "type": "string" + }, + "spec": { + "title": "spec", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositorySpec" + } + }, + "title": "trpc.ocmanager.repository.RepositoryInventoryImportItem", + "description": "RepositoryInventoryImportItem" + }, + "trpc.ocmanager.repository.RepositoryInventoryImportJob": { + "type": "object", + "properties": { + "conflict_strategy": { + "title": "conflict_strategy", + "type": "string" + }, + "created_at": { + "title": "created_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "error_code": { + "title": "error_code", + "type": "string" + }, + "error_message": { + "title": "error_message", + "type": "string" + }, + "finished_at": { + "title": "finished_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "id": { + "title": "id", + "type": "string" + }, + "preview_id": { + "title": "preview_id", + "type": "string" + }, + "project_id": { + "title": "project_id", + "type": "string", + "format": "int64" + }, + "repository_task_id": { + "title": "repository_task_id", + "type": "string" + }, + "requested_by": { + "title": "requested_by", + "type": "string" + }, + "schema_version": { + "title": "schema_version", + "type": "string" + }, + "status": { + "title": "status", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryInventoryImportJob", + "description": "RepositoryInventoryImportJob" + }, + "trpc.ocmanager.repository.RepositoryInventoryImportPreviewResponse": { + "type": "object", + "properties": { + "expires_at": { + "title": "expires_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "items": { + "title": "items", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryInventoryImportItem" + } + }, + "preview_digest": { + "title": "preview_digest", + "type": "string" + }, + "preview_id": { + "title": "preview_id", + "type": "string" + }, + "resource_version": { + "title": "resource_version", + "type": "string" + }, + "schema_version": { + "title": "schema_version", + "type": "string" + }, + "status": { + "title": "status", + "type": "string" + }, + "warnings": { + "title": "warnings", + "type": "array", + "items": { + "type": "string" + } + } + }, + "title": "trpc.ocmanager.repository.RepositoryInventoryImportPreviewResponse", + "description": "RepositoryInventoryImportPreviewResponse" + }, + "trpc.ocmanager.repository.RepositoryInventoryImportResponse": { + "type": "object", + "properties": { + "job": { + "title": "job", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryInventoryImportJob" + }, + "repositories": { + "title": "repositories", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.Repository" + } + } + }, + "title": "trpc.ocmanager.repository.RepositoryInventoryImportResponse", + "description": "RepositoryInventoryImportResponse" + }, + "trpc.ocmanager.repository.RepositoryInventorySigningKeyMapping": { + "type": "object", + "properties": { + "source_key_ref": { + "title": "source_key_ref", + "type": "string" + }, + "target_key_ref": { + "title": "target_key_ref", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryInventorySigningKeyMapping", + "description": "RepositoryInventorySigningKeyMapping" + }, + "trpc.ocmanager.repository.RepositoryInventoryStorageNodeMapping": { + "type": "object", + "properties": { + "source_ref": { + "title": "source_ref", + "type": "string" + }, + "target_storage_node_id": { + "title": "target_storage_node_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryInventoryStorageNodeMapping", + "description": "RepositoryInventoryStorageNodeMapping" + }, + "trpc.ocmanager.repository.RepositoryMetadataSigningKey": { + "type": "object", + "properties": { + "fingerprint": { + "title": "fingerprint", + "type": "string" + }, + "id": { + "title": "id", + "type": "string" + }, + "key_created_at": { + "title": "key_created_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "key_ref": { + "title": "key_ref", + "type": "string" + }, + "observed_at": { + "title": "observed_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "public_key_name": { + "title": "public_key_name", + "type": "string" + }, + "public_key_url": { + "title": "public_key_url", + "type": "string" + }, + "status": { + "title": "status", + "type": "string" + }, + "storage_node_id": { + "title": "storage_node_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryMetadataSigningKey", + "description": "RepositoryMetadataSigningKey" + }, + "trpc.ocmanager.repository.RepositoryNodeArtifact": { + "type": "object", + "properties": { + "architecture": { + "title": "architecture", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_ARCHITECTURE_UNSPECIFIED - \n * 1 - REPOSITORY_ARCHITECTURE_X86_64 - \n * 2 - REPOSITORY_ARCHITECTURE_AARCH64 - \n * 3 - REPOSITORY_ARCHITECTURE_NOARCH - \n", + "enum": [ + 0, + 1, + 2, + 3 + ] + }, + "build_commit": { + "title": "build_commit", + "type": "string" + }, + "created_at": { + "title": "created_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "epoch": { + "title": "epoch", + "type": "string", + "format": "uint64" + }, + "file_name": { + "title": "file_name", + "type": "string" + }, + "id": { + "title": "id", + "type": "string" + }, + "immutable_url": { + "title": "immutable_url", + "type": "string" + }, + "name": { + "title": "name", + "type": "string" + }, + "nevra": { + "title": "nevra", + "type": "string" + }, + "package_id": { + "title": "package_id", + "type": "string" + }, + "project_id": { + "title": "project_id", + "type": "string", + "format": "int64" + }, + "relative_path": { + "title": "relative_path", + "type": "string" + }, + "release": { + "title": "release", + "type": "string" + }, + "revision_id": { + "title": "revision_id", + "type": "string" + }, + "sha256": { + "title": "sha256", + "type": "string" + }, + "size_bytes": { + "title": "size_bytes", + "type": "string", + "format": "int64" + }, + "source_kind": { + "title": "source_kind", + "type": "string" + }, + "source_repository_id": { + "title": "source_repository_id", + "type": "string" + }, + "status": { + "title": "status", + "type": "string" + }, + "version": { + "title": "version", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryNodeArtifact", + "description": "RepositoryNodeArtifact" + }, + "trpc.ocmanager.repository.RepositoryNodeArtifactResponse": { + "type": "object", + "properties": { + "artifact": { + "title": "artifact", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryNodeArtifact" + } + }, + "title": "trpc.ocmanager.repository.RepositoryNodeArtifactResponse", + "description": "RepositoryNodeArtifactResponse" + }, + "trpc.ocmanager.repository.RepositoryNodeArtifactSpec": { + "type": "object", + "properties": { + "architecture": { + "title": "architecture", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_ARCHITECTURE_UNSPECIFIED - \n * 1 - REPOSITORY_ARCHITECTURE_X86_64 - \n * 2 - REPOSITORY_ARCHITECTURE_AARCH64 - \n * 3 - REPOSITORY_ARCHITECTURE_NOARCH - \n", + "enum": [ + 0, + 1, + 2, + 3 + ] + }, + "build_commit": { + "title": "build_commit", + "type": "string" + }, + "file_name": { + "title": "file_name", + "type": "string" + }, + "name": { + "title": "name", + "type": "string" + }, + "package_id": { + "title": "package_id", + "type": "string" + }, + "project_id": { + "title": "project_id", + "type": "string", + "format": "int64" + }, + "relative_path": { + "title": "relative_path", + "type": "string" + }, + "release": { + "title": "release", + "type": "string" + }, + "revision_id": { + "title": "revision_id", + "type": "string" + }, + "sha256": { + "title": "sha256", + "type": "string" + }, + "size_bytes": { + "title": "size_bytes", + "type": "string", + "format": "int64" + }, + "source_repository_id": { + "title": "source_repository_id", + "type": "string" + }, + "version": { + "title": "version", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryNodeArtifactSpec", + "description": "RepositoryNodeArtifactSpec" + }, + "trpc.ocmanager.repository.RepositoryNodeEnrollment": { + "type": "object", + "properties": { + "allowlist_entry_id": { + "title": "allowlist_entry_id", + "type": "string" + }, + "artifact_id": { + "title": "artifact_id", + "type": "string" + }, + "authorization_id": { + "title": "authorization_id", + "type": "string" + }, + "created_at": { + "title": "created_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "created_by": { + "title": "created_by", + "type": "string" + }, + "csr_sha256": { + "title": "csr_sha256", + "type": "string" + }, + "expected_architecture": { + "title": "expected_architecture", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_ARCHITECTURE_UNSPECIFIED - \n * 1 - REPOSITORY_ARCHITECTURE_X86_64 - \n * 2 - REPOSITORY_ARCHITECTURE_AARCH64 - \n * 3 - REPOSITORY_ARCHITECTURE_NOARCH - \n", + "enum": [ + 0, + 1, + 2, + 3 + ] + }, + "expires_at": { + "title": "expires_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "generation": { + "title": "generation", + "type": "integer", + "format": "int32" + }, + "id": { + "title": "id", + "type": "string" + }, + "install_task_id": { + "title": "install_task_id", + "type": "string" + }, + "issuance_status": { + "title": "issuance_status", + "type": "string" + }, + "last_error_code": { + "title": "last_error_code", + "type": "string" + }, + "last_error_message": { + "title": "last_error_message", + "type": "string" + }, + "project_id": { + "title": "project_id", + "type": "string", + "format": "int64" + }, + "status": { + "title": "status", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_NODE_ENROLLMENT_STATUS_UNSPECIFIED - \n * 1 - REPOSITORY_NODE_ENROLLMENT_STATUS_PENDING - \n * 2 - REPOSITORY_NODE_ENROLLMENT_STATUS_INSTALLING - \n * 3 - REPOSITORY_NODE_ENROLLMENT_STATUS_BOOTSTRAPPED - \n * 4 - REPOSITORY_NODE_ENROLLMENT_STATUS_ONLINE - \n * 5 - REPOSITORY_NODE_ENROLLMENT_STATUS_FAILED - \n * 6 - REPOSITORY_NODE_ENROLLMENT_STATUS_EXPIRED - \n * 7 - REPOSITORY_NODE_ENROLLMENT_STATUS_AUTHORIZED - \n * 8 - REPOSITORY_NODE_ENROLLMENT_STATUS_REGISTERING - \n * 9 - REPOSITORY_NODE_ENROLLMENT_STATUS_REGISTERED - \n", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + }, + "storage_node_id": { + "title": "storage_node_id", + "type": "string" + }, + "tms_agent_id": { + "title": "tms_agent_id", + "type": "string", + "format": "uint64" + }, + "tms_task_id": { + "title": "tms_task_id", + "type": "string" + }, + "updated_at": { + "title": "updated_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + } + }, + "title": "trpc.ocmanager.repository.RepositoryNodeEnrollment", + "description": "RepositoryNodeEnrollment" + }, + "trpc.ocmanager.repository.RepositoryNodeEnrollmentResponse": { + "type": "object", + "properties": { + "bootstrap_expires_at": { + "title": "bootstrap_expires_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "bootstrap_token": { + "title": "bootstrap_token", + "type": "string" + }, + "enrollment": { + "title": "enrollment", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryNodeEnrollment" + }, + "storage_node": { + "title": "storage_node", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryStorageNode" + }, + "target": { + "title": "target", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryTaskTarget" + }, + "task": { + "title": "task", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryTask" + } + }, + "title": "trpc.ocmanager.repository.RepositoryNodeEnrollmentResponse", + "description": "RepositoryNodeEnrollmentResponse" + }, + "trpc.ocmanager.repository.RepositoryNodeEnrollmentSpec": { + "type": "object", + "properties": { + "artifact_id": { + "title": "artifact_id", + "type": "string" + }, + "control_base_url": { + "title": "control_base_url", + "type": "string" + }, + "description": { + "title": "description", + "type": "string" + }, + "expected_architecture": { + "title": "expected_architecture", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_ARCHITECTURE_UNSPECIFIED - \n * 1 - REPOSITORY_ARCHITECTURE_X86_64 - \n * 2 - REPOSITORY_ARCHITECTURE_AARCH64 - \n * 3 - REPOSITORY_ARCHITECTURE_NOARCH - \n", + "enum": [ + 0, + 1, + 2, + 3 + ] + }, + "labels": { + "title": "labels", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryNodeEnrollmentSpec.LabelsEntry" + } + }, + "name": { + "title": "name", + "type": "string" + }, + "project_id": { + "title": "project_id", + "type": "string", + "format": "int64" + }, + "publish_base_url": { + "title": "publish_base_url", + "type": "string" + }, + "tls_server_name": { + "title": "tls_server_name", + "type": "string" + }, + "tms_agent_id": { + "title": "tms_agent_id", + "type": "string", + "format": "uint64" + } + }, + "title": "trpc.ocmanager.repository.RepositoryNodeEnrollmentSpec", + "description": "RepositoryNodeEnrollmentSpec" + }, + "trpc.ocmanager.repository.RepositoryNodeEnrollmentSpec.LabelsEntry": { + "type": "object", + "properties": { + "key": { + "title": "key", + "type": "string" + }, + "value": { + "title": "value", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryNodeEnrollmentSpec.LabelsEntry", + "description": "LabelsEntry" + }, + "trpc.ocmanager.repository.RepositoryNodeInstallSource": { + "type": "object", + "properties": { + "architecture": { + "title": "architecture", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_ARCHITECTURE_UNSPECIFIED - \n * 1 - REPOSITORY_ARCHITECTURE_X86_64 - \n * 2 - REPOSITORY_ARCHITECTURE_AARCH64 - \n * 3 - REPOSITORY_ARCHITECTURE_NOARCH - \n", + "enum": [ + 0, + 1, + 2, + 3 + ] + }, + "created_at": { + "title": "created_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "created_by": { + "title": "created_by", + "type": "string" + }, + "id": { + "title": "id", + "type": "string" + }, + "project_id": { + "title": "project_id", + "type": "string", + "format": "int64" + }, + "repository_id": { + "title": "repository_id", + "type": "string" + }, + "status": { + "title": "status", + "type": "string" + }, + "updated_at": { + "title": "updated_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "updated_by": { + "title": "updated_by", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryNodeInstallSource", + "description": "RepositoryNodeInstallSource" + }, + "trpc.ocmanager.repository.RepositoryNodeInstallSourceResponse": { + "type": "object", + "properties": { + "install_source": { + "title": "install_source", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryNodeInstallSource" + } + }, + "title": "trpc.ocmanager.repository.RepositoryNodeInstallSourceResponse", + "description": "RepositoryNodeInstallSourceResponse" + }, + "trpc.ocmanager.repository.RepositoryNodeInstallSourceSpec": { + "type": "object", + "properties": { + "architecture": { + "title": "architecture", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_ARCHITECTURE_UNSPECIFIED - \n * 1 - REPOSITORY_ARCHITECTURE_X86_64 - \n * 2 - REPOSITORY_ARCHITECTURE_AARCH64 - \n * 3 - REPOSITORY_ARCHITECTURE_NOARCH - \n", + "enum": [ + 0, + 1, + 2, + 3 + ] + }, + "project_id": { + "title": "project_id", + "type": "string", + "format": "int64" + }, + "repository_id": { + "title": "repository_id", + "type": "string" + }, + "status": { + "title": "status", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryNodeInstallSourceSpec", + "description": "RepositoryNodeInstallSourceSpec" + }, + "trpc.ocmanager.repository.RepositoryNodeRegistrationAllowlist": { + "type": "object", + "properties": { + "created_at": { + "title": "created_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "created_by": { + "title": "created_by", + "type": "string" + }, + "expected_architecture": { + "title": "expected_architecture", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_ARCHITECTURE_UNSPECIFIED - \n * 1 - REPOSITORY_ARCHITECTURE_X86_64 - \n * 2 - REPOSITORY_ARCHITECTURE_AARCH64 - \n * 3 - REPOSITORY_ARCHITECTURE_NOARCH - \n", + "enum": [ + 0, + 1, + 2, + 3 + ] + }, + "expected_hostname": { + "title": "expected_hostname", + "type": "string" + }, + "expected_ip": { + "title": "expected_ip", + "type": "string" + }, + "expires_at": { + "title": "expires_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "id": { + "title": "id", + "type": "string" + }, + "project_id": { + "title": "project_id", + "type": "string", + "format": "int64" + }, + "status": { + "title": "status", + "type": "string" + }, + "tms_agent_id": { + "title": "tms_agent_id", + "type": "string", + "format": "uint64" + }, + "updated_at": { + "title": "updated_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "updated_by": { + "title": "updated_by", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryNodeRegistrationAllowlist", + "description": "RepositoryNodeRegistrationAllowlist" + }, + "trpc.ocmanager.repository.RepositoryNodeRegistrationAllowlistResponse": { + "type": "object", + "properties": { + "allowlist": { + "title": "allowlist", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryNodeRegistrationAllowlist" + } + }, + "title": "trpc.ocmanager.repository.RepositoryNodeRegistrationAllowlistResponse", + "description": "RepositoryNodeRegistrationAllowlistResponse" + }, + "trpc.ocmanager.repository.RepositoryNodeRegistrationAllowlistSpec": { + "type": "object", + "properties": { + "expected_architecture": { + "title": "expected_architecture", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_ARCHITECTURE_UNSPECIFIED - \n * 1 - REPOSITORY_ARCHITECTURE_X86_64 - \n * 2 - REPOSITORY_ARCHITECTURE_AARCH64 - \n * 3 - REPOSITORY_ARCHITECTURE_NOARCH - \n", + "enum": [ + 0, + 1, + 2, + 3 + ] + }, + "expected_hostname": { + "title": "expected_hostname", + "type": "string" + }, + "expected_ip": { + "title": "expected_ip", + "type": "string" + }, + "expires_at": { + "title": "expires_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "project_id": { + "title": "project_id", + "type": "string", + "format": "int64" + }, + "status": { + "title": "status", + "type": "string" + }, + "tms_agent_id": { + "title": "tms_agent_id", + "type": "string", + "format": "uint64" + } + }, + "title": "trpc.ocmanager.repository.RepositoryNodeRegistrationAllowlistSpec", + "description": "RepositoryNodeRegistrationAllowlistSpec" + }, + "trpc.ocmanager.repository.RepositoryNodeRepositorySummary": { + "type": "object", + "properties": { + "architecture": { + "title": "architecture", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_ARCHITECTURE_UNSPECIFIED - \n * 1 - REPOSITORY_ARCHITECTURE_X86_64 - \n * 2 - REPOSITORY_ARCHITECTURE_AARCH64 - \n * 3 - REPOSITORY_ARCHITECTURE_NOARCH - \n", + "enum": [ + 0, + 1, + 2, + 3 + ] + }, + "current_revision": { + "title": "current_revision", + "type": "string" + }, + "last_sync_at": { + "title": "last_sync_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "last_sync_error_code": { + "title": "last_sync_error_code", + "type": "string" + }, + "last_sync_error_message": { + "title": "last_sync_error_message", + "type": "string" + }, + "last_sync_error_zh_message": { + "title": "last_sync_error_zh_message", + "type": "string" + }, + "last_sync_error_zh_recommended_action": { + "title": "last_sync_error_zh_recommended_action", + "type": "string" + }, + "last_sync_status": { + "title": "last_sync_status", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_TASK_STATUS_UNSPECIFIED - \n * 1 - REPOSITORY_TASK_STATUS_PENDING - \n * 2 - REPOSITORY_TASK_STATUS_RUNNING - \n * 3 - REPOSITORY_TASK_STATUS_CANCEL_REQUESTED - \n * 4 - REPOSITORY_TASK_STATUS_CANCELED - \n * 5 - REPOSITORY_TASK_STATUS_SUCCESS - \n * 6 - REPOSITORY_TASK_STATUS_FAILED - \n * 7 - REPOSITORY_TASK_STATUS_PARTIAL_SUCCESS - \n", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ] + }, + "logical_bytes": { + "title": "logical_bytes", + "type": "string", + "format": "int64" + }, + "name": { + "title": "name", + "type": "string" + }, + "next_sync_at": { + "title": "next_sync_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "package_count": { + "title": "package_count", + "type": "string", + "format": "int64" + }, + "physical_bytes": { + "title": "physical_bytes", + "type": "string", + "format": "int64" + }, + "product_version": { + "title": "product_version", + "type": "string" + }, + "publish_status": { + "title": "publish_status", + "type": "string" + }, + "repository_id": { + "title": "repository_id", + "type": "string" + }, + "repository_type": { + "title": "repository_type", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_TYPE_UNSPECIFIED - \n * 1 - REPOSITORY_TYPE_BASELINE - \n * 2 - REPOSITORY_TYPE_UPDATE - \n * 3 - REPOSITORY_TYPE_PRIVATE - \n", + "enum": [ + 0, + 1, + 2, + 3 + ] + }, + "running_task_id": { + "title": "running_task_id", + "type": "string" + }, + "system_product": { + "title": "system_product", + "type": "string" + }, + "upstream_url": { + "title": "upstream_url", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryNodeRepositorySummary", + "description": "RepositoryNodeRepositorySummary" + }, + "trpc.ocmanager.repository.RepositoryOCAIAnalysis": { + "type": "object", + "properties": { + "action_result": { + "title": "action_result", + "type": "string" + }, + "action_task_id": { + "title": "action_task_id", + "type": "string" + }, + "context_sha256": { + "title": "context_sha256", + "type": "string" + }, + "created_at": { + "title": "created_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "feedback_at": { + "title": "feedback_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "id": { + "title": "id", + "type": "string" + }, + "requested_by": { + "title": "requested_by", + "type": "string" + }, + "session_id": { + "title": "session_id", + "type": "string" + }, + "status": { + "title": "status", + "type": "string" + }, + "task_id": { + "title": "task_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryOCAIAnalysis", + "description": "RepositoryOCAIAnalysis" + }, + "trpc.ocmanager.repository.RepositoryOCAIAnalysisResponse": { + "type": "object", + "properties": { + "analysis": { + "title": "analysis", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryOCAIAnalysis" + }, + "context": { + "title": "context", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryOCAIContext" + }, + "instruction": { + "title": "instruction", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryOCAIAnalysisResponse", + "description": "RepositoryOCAIAnalysisResponse" + }, + "trpc.ocmanager.repository.RepositoryOCAIContext": { + "type": "object", + "properties": { + "architecture": { + "title": "architecture", + "type": "string" + }, + "context_json": { + "title": "context_json", + "type": "string" + }, + "context_sha256": { + "title": "context_sha256", + "type": "string" + }, + "current_step": { + "title": "current_step", + "type": "string" + }, + "diagnosis": { + "title": "diagnosis", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryDiagnosis" + }, + "error_code": { + "title": "error_code", + "type": "string" + }, + "error_message": { + "title": "error_message", + "type": "string" + }, + "log_summary": { + "title": "log_summary", + "type": "string" + }, + "product_version": { + "title": "product_version", + "type": "string" + }, + "repository_id": { + "title": "repository_id", + "type": "string" + }, + "repository_name": { + "title": "repository_name", + "type": "string" + }, + "revision_id": { + "title": "revision_id", + "type": "string" + }, + "storage_node_id": { + "title": "storage_node_id", + "type": "string" + }, + "system_product": { + "title": "system_product", + "type": "string" + }, + "task_id": { + "title": "task_id", + "type": "string" + }, + "task_status": { + "title": "task_status", + "type": "string" + }, + "task_type": { + "title": "task_type", + "type": "string" + }, + "truncated": { + "title": "truncated", + "type": "boolean" + } + }, + "title": "trpc.ocmanager.repository.RepositoryOCAIContext", + "description": "RepositoryOCAIContext" + }, + "trpc.ocmanager.repository.RepositoryOCAIContextResponse": { + "type": "object", + "properties": { + "context": { + "title": "context", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryOCAIContext" + } + }, + "title": "trpc.ocmanager.repository.RepositoryOCAIContextResponse", + "description": "RepositoryOCAIContextResponse" + }, + "trpc.ocmanager.repository.RepositoryPackage": { + "type": "object", + "properties": { + "architecture": { + "title": "architecture", + "type": "string" + }, + "created_at": { + "title": "created_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "epoch": { + "title": "epoch", + "type": "string" + }, + "file_name": { + "title": "file_name", + "type": "string" + }, + "id": { + "title": "id", + "type": "string" + }, + "name": { + "title": "name", + "type": "string" + }, + "nevra": { + "title": "nevra", + "type": "string" + }, + "release": { + "title": "release", + "type": "string" + }, + "repository_id": { + "title": "repository_id", + "type": "string" + }, + "revision_id": { + "title": "revision_id", + "type": "string" + }, + "sha256": { + "title": "sha256", + "type": "string" + }, + "signature_message": { + "title": "signature_message", + "type": "string" + }, + "signature_status": { + "title": "signature_status", + "type": "string" + }, + "signer_key_id": { + "title": "signer_key_id", + "type": "string" + }, + "size_bytes": { + "title": "size_bytes", + "type": "string", + "format": "int64" + }, + "source": { + "title": "source", + "type": "string" + }, + "version": { + "title": "version", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryPackage", + "description": "RepositoryPackage" + }, + "trpc.ocmanager.repository.RepositoryPackageDeletionPreview": { + "type": "object", + "properties": { + "command": { + "title": "command", + "type": "array", + "items": { + "type": "string" + } + }, + "created_by": { + "title": "created_by", + "type": "string" + }, + "dependency_impact": { + "title": "dependency_impact", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryPackageDependencyImpact" + } + }, + "expires_at": { + "title": "expires_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "nevras": { + "title": "nevras", + "type": "array", + "items": { + "type": "string" + } + }, + "preview_digest": { + "title": "preview_digest", + "type": "string" + }, + "preview_id": { + "title": "preview_id", + "type": "string" + }, + "raw_output": { + "title": "raw_output", + "type": "string" + }, + "repository_id": { + "title": "repository_id", + "type": "string" + }, + "repository_set": { + "title": "repository_set", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryPackageDeletionPreviewSource" + } + }, + "revision_id": { + "title": "revision_id", + "type": "string" + }, + "status": { + "title": "status", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryPackageDeletionPreview", + "description": "RepositoryPackageDeletionPreview" + }, + "trpc.ocmanager.repository.RepositoryPackageDeletionPreviewResponse": { + "type": "object", + "properties": { + "preview": { + "title": "preview", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryPackageDeletionPreview" + } + }, + "title": "trpc.ocmanager.repository.RepositoryPackageDeletionPreviewResponse", + "description": "RepositoryPackageDeletionPreviewResponse" + }, + "trpc.ocmanager.repository.RepositoryPackageDeletionPreviewSource": { + "type": "object", + "properties": { + "baseurl": { + "title": "baseurl", + "type": "string" + }, + "repo_id": { + "title": "repo_id", + "type": "string" + }, + "repository_id": { + "title": "repository_id", + "type": "string" + }, + "revision_id": { + "title": "revision_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryPackageDeletionPreviewSource", + "description": "RepositoryPackageDeletionPreviewSource" + }, + "trpc.ocmanager.repository.RepositoryPackageDependencyImpact": { + "type": "object", + "properties": { + "depth": { + "title": "depth", + "type": "integer", + "format": "int32" + }, + "direct": { + "title": "direct", + "type": "boolean" + }, + "package_nevra": { + "title": "package_nevra", + "type": "string" + }, + "reason": { + "title": "reason", + "type": "string" + }, + "requires": { + "title": "requires", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryPackageDependencyImpact", + "description": "RepositoryPackageDependencyImpact" + }, + "trpc.ocmanager.repository.RepositoryPackageInstall": { + "type": "object", + "properties": { + "created_at": { + "title": "created_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "id": { + "title": "id", + "type": "string" + }, + "preview_id": { + "title": "preview_id", + "type": "string" + }, + "project_id": { + "title": "project_id", + "type": "string", + "format": "int64" + }, + "repository_id": { + "title": "repository_id", + "type": "string" + }, + "repository_task_id": { + "title": "repository_task_id", + "type": "string" + }, + "requested_by": { + "title": "requested_by", + "type": "string" + }, + "revision_id": { + "title": "revision_id", + "type": "string" + }, + "status": { + "title": "status", + "type": "string" + }, + "timeout_seconds": { + "title": "timeout_seconds", + "type": "integer", + "format": "int32" + }, + "tms_task_id": { + "title": "tms_task_id", + "type": "string" + }, + "updated_at": { + "title": "updated_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + } + }, + "title": "trpc.ocmanager.repository.RepositoryPackageInstall", + "description": "RepositoryPackageInstall" + }, + "trpc.ocmanager.repository.RepositoryPackageInstallPreview": { + "type": "object", + "properties": { + "created_by": { + "title": "created_by", + "type": "string" + }, + "expires_at": { + "title": "expires_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "package_names": { + "title": "package_names", + "type": "array", + "items": { + "type": "string" + } + }, + "preview_digest": { + "title": "preview_digest", + "type": "string" + }, + "preview_id": { + "title": "preview_id", + "type": "string" + }, + "repo_id": { + "title": "repo_id", + "type": "string" + }, + "repository_id": { + "title": "repository_id", + "type": "string" + }, + "revision_id": { + "title": "revision_id", + "type": "string" + }, + "status": { + "title": "status", + "type": "string" + }, + "targets": { + "title": "targets", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryPackageInstallPreviewTarget" + } + } + }, + "title": "trpc.ocmanager.repository.RepositoryPackageInstallPreview", + "description": "RepositoryPackageInstallPreview" + }, + "trpc.ocmanager.repository.RepositoryPackageInstallPreviewResponse": { + "type": "object", + "properties": { + "preview": { + "title": "preview", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryPackageInstallPreview" + } + }, + "title": "trpc.ocmanager.repository.RepositoryPackageInstallPreviewResponse", + "description": "RepositoryPackageInstallPreviewResponse" + }, + "trpc.ocmanager.repository.RepositoryPackageInstallPreviewTarget": { + "type": "object", + "properties": { + "agent_id": { + "title": "agent_id", + "type": "string" + }, + "dependency_packages": { + "title": "dependency_packages", + "type": "array", + "items": { + "type": "string" + } + }, + "disk_change_bytes": { + "title": "disk_change_bytes", + "type": "string", + "format": "int64" + }, + "download_bytes": { + "title": "download_bytes", + "type": "string", + "format": "int64" + }, + "error_code": { + "title": "error_code", + "type": "string" + }, + "error_message": { + "title": "error_message", + "type": "string" + }, + "exit_code": { + "title": "exit_code", + "type": "integer", + "format": "int32" + }, + "finished_at": { + "title": "finished_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "ip": { + "title": "ip", + "type": "string" + }, + "output_truncated": { + "title": "output_truncated", + "type": "boolean" + }, + "raw_output": { + "title": "raw_output", + "type": "string" + }, + "replacement_packages": { + "title": "replacement_packages", + "type": "array", + "items": { + "type": "string" + } + }, + "selected_packages": { + "title": "selected_packages", + "type": "array", + "items": { + "type": "string" + } + }, + "started_at": { + "title": "started_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "status": { + "title": "status", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_TASK_STATUS_UNSPECIFIED - \n * 1 - REPOSITORY_TASK_STATUS_PENDING - \n * 2 - REPOSITORY_TASK_STATUS_RUNNING - \n * 3 - REPOSITORY_TASK_STATUS_CANCEL_REQUESTED - \n * 4 - REPOSITORY_TASK_STATUS_CANCELED - \n * 5 - REPOSITORY_TASK_STATUS_SUCCESS - \n * 6 - REPOSITORY_TASK_STATUS_FAILED - \n * 7 - REPOSITORY_TASK_STATUS_PARTIAL_SUCCESS - \n", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ] + }, + "upgrade_packages": { + "title": "upgrade_packages", + "type": "array", + "items": { + "type": "string" + } + } + }, + "title": "trpc.ocmanager.repository.RepositoryPackageInstallPreviewTarget", + "description": "RepositoryPackageInstallPreviewTarget" + }, + "trpc.ocmanager.repository.RepositoryPackageInstallResponse": { + "type": "object", + "properties": { + "install": { + "title": "install", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryPackageInstall" + }, + "task": { + "title": "task", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryTask" + } + }, + "title": "trpc.ocmanager.repository.RepositoryPackageInstallResponse", + "description": "RepositoryPackageInstallResponse" + }, + "trpc.ocmanager.repository.RepositoryPreflightRecord": { + "type": "object", + "properties": { + "capacity_bytes": { + "title": "capacity_bytes", + "type": "string", + "format": "int64" + }, + "created_at": { + "title": "created_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "error_code": { + "title": "error_code", + "type": "string" + }, + "error_message": { + "title": "error_message", + "type": "string" + }, + "estimated_import_bytes": { + "title": "estimated_import_bytes", + "type": "string", + "format": "int64" + }, + "estimated_package_count": { + "title": "estimated_package_count", + "type": "string", + "format": "int64" + }, + "expires_at": { + "title": "expires_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "filesystem_type": { + "title": "filesystem_type", + "type": "string" + }, + "free_bytes": { + "title": "free_bytes", + "type": "string", + "format": "int64" + }, + "free_inodes": { + "title": "free_inodes", + "type": "string", + "format": "int64" + }, + "id": { + "title": "id", + "type": "string" + }, + "repository_id": { + "title": "repository_id", + "type": "string" + }, + "required_free_bytes": { + "title": "required_free_bytes", + "type": "string", + "format": "int64" + }, + "required_inodes": { + "title": "required_inodes", + "type": "string", + "format": "int64" + }, + "status": { + "title": "status", + "type": "string" + }, + "storage_node_id": { + "title": "storage_node_id", + "type": "string" + }, + "task_id": { + "title": "task_id", + "type": "string" + }, + "tool_versions": { + "title": "tool_versions", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryPreflightRecord.ToolVersionsEntry" + } + } + }, + "title": "trpc.ocmanager.repository.RepositoryPreflightRecord", + "description": "RepositoryPreflightRecord" + }, + "trpc.ocmanager.repository.RepositoryPreflightRecord.ToolVersionsEntry": { + "type": "object", + "properties": { + "key": { + "title": "key", + "type": "string" + }, + "value": { + "title": "value", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryPreflightRecord.ToolVersionsEntry", + "description": "ToolVersionsEntry" + }, + "trpc.ocmanager.repository.RepositoryQuarantineItem": { + "type": "object", + "properties": { + "created_at": { + "title": "created_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "detected_nevra": { + "title": "detected_nevra", + "type": "string" + }, + "id": { + "title": "id", + "type": "string" + }, + "original_name": { + "title": "original_name", + "type": "string" + }, + "reason_code": { + "title": "reason_code", + "type": "string" + }, + "reason_message": { + "title": "reason_message", + "type": "string" + }, + "repository_id": { + "title": "repository_id", + "type": "string" + }, + "sha256": { + "title": "sha256", + "type": "string" + }, + "size_bytes": { + "title": "size_bytes", + "type": "string", + "format": "int64" + }, + "task_id": { + "title": "task_id", + "type": "string" + }, + "upload_id": { + "title": "upload_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryQuarantineItem", + "description": "RepositoryQuarantineItem" + }, + "trpc.ocmanager.repository.RepositoryResponse": { + "type": "object", + "properties": { + "repository": { + "title": "repository", + "$ref": "#/definitions/trpc.ocmanager.repository.Repository" + } + }, + "title": "trpc.ocmanager.repository.RepositoryResponse", + "description": "RepositoryResponse" + }, + "trpc.ocmanager.repository.RepositorySpec": { + "type": "object", + "properties": { + "allow_upload": { + "title": "allow_upload", + "type": "boolean" + }, + "architecture": { + "title": "architecture", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_ARCHITECTURE_UNSPECIFIED - \n * 1 - REPOSITORY_ARCHITECTURE_X86_64 - \n * 2 - REPOSITORY_ARCHITECTURE_AARCH64 - \n * 3 - REPOSITORY_ARCHITECTURE_NOARCH - \n", + "enum": [ + 0, + 1, + 2, + 3 + ] + }, + "bandwidth_limit_bytes": { + "title": "bandwidth_limit_bytes", + "type": "string", + "format": "int64" + }, + "description": { + "title": "description", + "type": "string" + }, + "enabled": { + "title": "enabled", + "type": "boolean" + }, + "exclude_packages": { + "title": "exclude_packages", + "type": "array", + "items": { + "type": "string" + } + }, + "gpg_policy": { + "title": "gpg_policy", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_GPG_POLICY_UNSPECIFIED - \n * 1 - REPOSITORY_GPG_POLICY_DISABLED - \n * 2 - REPOSITORY_GPG_POLICY_VERIFY_PACKAGE - \n * 3 - REPOSITORY_GPG_POLICY_AUDIT - \n * 4 - REPOSITORY_GPG_POLICY_ENFORCE - \n", + "enum": [ + 0, + 1, + 2, + 3, + 4 + ] + }, + "include_packages": { + "title": "include_packages", + "type": "array", + "items": { + "type": "string" + } + }, + "max_parallel_downloads": { + "title": "max_parallel_downloads", + "type": "integer", + "format": "int32" + }, + "metadata_signing_key_ref": { + "title": "metadata_signing_key_ref", + "type": "string" + }, + "name": { + "title": "name", + "type": "string" + }, + "primary_storage_node_id": { + "title": "primary_storage_node_id", + "type": "string" + }, + "product_version": { + "title": "product_version", + "type": "string" + }, + "project_id": { + "title": "project_id", + "type": "string", + "format": "int64" + }, + "sync_mode": { + "title": "sync_mode", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_SYNC_MODE_UNSPECIFIED - \n * 1 - REPOSITORY_SYNC_MODE_MIRROR - \n * 2 - REPOSITORY_SYNC_MODE_APPEND - \n", + "enum": [ + 0, + 1, + 2 + ] + }, + "system_product": { + "title": "system_product", + "type": "string" + }, + "type": { + "title": "type", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_TYPE_UNSPECIFIED - \n * 1 - REPOSITORY_TYPE_BASELINE - \n * 2 - REPOSITORY_TYPE_UPDATE - \n * 3 - REPOSITORY_TYPE_PRIVATE - \n", + "enum": [ + 0, + 1, + 2, + 3 + ] + }, + "upstream_url": { + "title": "upstream_url", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositorySpec", + "description": "RepositorySpec" + }, + "trpc.ocmanager.repository.RepositoryStorageNode": { + "type": "object", + "properties": { + "agent_version": { + "title": "agent_version", + "type": "string" + }, + "architecture": { + "title": "architecture", + "type": "string" + }, + "capacity_bytes": { + "title": "capacity_bytes", + "type": "string", + "format": "int64" + }, + "certificate_fingerprint": { + "title": "certificate_fingerprint", + "type": "string" + }, + "control_base_url": { + "title": "control_base_url", + "type": "string" + }, + "created_at": { + "title": "created_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "created_by": { + "title": "created_by", + "type": "string" + }, + "description": { + "title": "description", + "type": "string" + }, + "disk_usage_percent": { + "title": "disk_usage_percent", + "type": "number", + "format": "double" + }, + "disk_used_bytes": { + "title": "disk_used_bytes", + "type": "string", + "format": "int64" + }, + "enabled": { + "title": "enabled", + "type": "boolean" + }, + "error_zh_message": { + "title": "error_zh_message", + "type": "string" + }, + "error_zh_recommended_action": { + "title": "error_zh_recommended_action", + "type": "string" + }, + "filesystem_type": { + "title": "filesystem_type", + "type": "string" + }, + "free_bytes": { + "title": "free_bytes", + "type": "string", + "format": "int64" + }, + "free_inodes": { + "title": "free_inodes", + "type": "string", + "format": "int64" + }, + "hostname": { + "title": "hostname", + "type": "string" + }, + "id": { + "title": "id", + "type": "string" + }, + "ip_address": { + "title": "ip_address", + "type": "string" + }, + "labels": { + "title": "labels", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryStorageNode.LabelsEntry" + } + }, + "last_error_code": { + "title": "last_error_code", + "type": "string" + }, + "last_error_message": { + "title": "last_error_message", + "type": "string" + }, + "last_heartbeat_at": { + "title": "last_heartbeat_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "last_probe_at": { + "title": "last_probe_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "name": { + "title": "name", + "type": "string" + }, + "os_name": { + "title": "os_name", + "type": "string" + }, + "os_version": { + "title": "os_version", + "type": "string" + }, + "project_id": { + "title": "project_id", + "type": "string", + "format": "int64" + }, + "publish_base_url": { + "title": "publish_base_url", + "type": "string" + }, + "repository_count": { + "title": "repository_count", + "type": "string", + "format": "int64" + }, + "repository_physical_bytes": { + "title": "repository_physical_bytes", + "type": "string", + "format": "int64" + }, + "status": { + "title": "status", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_STORAGE_NODE_STATUS_UNSPECIFIED - \n * 1 - REPOSITORY_STORAGE_NODE_STATUS_UNKNOWN - \n * 2 - REPOSITORY_STORAGE_NODE_STATUS_ONLINE - \n * 3 - REPOSITORY_STORAGE_NODE_STATUS_OFFLINE - \n * 4 - REPOSITORY_STORAGE_NODE_STATUS_DISABLED - \n * 5 - REPOSITORY_STORAGE_NODE_STATUS_PENDING - \n * 6 - REPOSITORY_STORAGE_NODE_STATUS_INSTALLING - \n * 7 - REPOSITORY_STORAGE_NODE_STATUS_INSTALL_FAILED - \n", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ] + }, + "tls_server_name": { + "title": "tls_server_name", + "type": "string" + }, + "tool_versions": { + "title": "tool_versions", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryStorageNode.ToolVersionsEntry" + } + }, + "updated_at": { + "title": "updated_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + } + }, + "title": "trpc.ocmanager.repository.RepositoryStorageNode", + "description": "RepositoryStorageNode" + }, + "trpc.ocmanager.repository.RepositoryStorageNode.LabelsEntry": { + "type": "object", + "properties": { + "key": { + "title": "key", + "type": "string" + }, + "value": { + "title": "value", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryStorageNode.LabelsEntry", + "description": "LabelsEntry" + }, + "trpc.ocmanager.repository.RepositoryStorageNode.ToolVersionsEntry": { + "type": "object", + "properties": { + "key": { + "title": "key", + "type": "string" + }, + "value": { + "title": "value", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryStorageNode.ToolVersionsEntry", + "description": "ToolVersionsEntry" + }, + "trpc.ocmanager.repository.RepositoryStorageNodeProbeResponse": { + "type": "object", + "properties": { + "preflight": { + "title": "preflight", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryPreflightRecord" + }, + "storage_node": { + "title": "storage_node", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryStorageNode" + } + }, + "title": "trpc.ocmanager.repository.RepositoryStorageNodeProbeResponse", + "description": "RepositoryStorageNodeProbeResponse" + }, + "trpc.ocmanager.repository.RepositoryStorageNodeResponse": { + "type": "object", + "properties": { + "metadata_signing_keys": { + "title": "metadata_signing_keys", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryMetadataSigningKey" + } + }, + "repositories": { + "title": "repositories", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryNodeRepositorySummary" + } + }, + "storage_node": { + "title": "storage_node", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryStorageNode" + } + }, + "title": "trpc.ocmanager.repository.RepositoryStorageNodeResponse", + "description": "RepositoryStorageNodeResponse" + }, + "trpc.ocmanager.repository.RepositoryStorageNodeSpec": { + "type": "object", + "properties": { + "certificate_fingerprint": { + "title": "certificate_fingerprint", + "type": "string" + }, + "control_base_url": { + "title": "control_base_url", + "type": "string" + }, + "description": { + "title": "description", + "type": "string" + }, + "enabled": { + "title": "enabled", + "type": "boolean" + }, + "labels": { + "title": "labels", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryStorageNodeSpec.LabelsEntry" + } + }, + "name": { + "title": "name", + "type": "string" + }, + "project_id": { + "title": "project_id", + "type": "string", + "format": "int64" + }, + "publish_base_url": { + "title": "publish_base_url", + "type": "string" + }, + "tls_server_name": { + "title": "tls_server_name", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryStorageNodeSpec", + "description": "RepositoryStorageNodeSpec" + }, + "trpc.ocmanager.repository.RepositoryStorageNodeSpec.LabelsEntry": { + "type": "object", + "properties": { + "key": { + "title": "key", + "type": "string" + }, + "value": { + "title": "value", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryStorageNodeSpec.LabelsEntry", + "description": "LabelsEntry" + }, + "trpc.ocmanager.repository.RepositorySyncSchedule": { + "type": "object", + "properties": { + "cadence": { + "title": "cadence", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_SYNC_SCHEDULE_CADENCE_UNSPECIFIED - \n * 1 - REPOSITORY_SYNC_SCHEDULE_CADENCE_DAILY - \n * 2 - REPOSITORY_SYNC_SCHEDULE_CADENCE_WEEKLY - \n", + "enum": [ + 0, + 1, + 2 + ] + }, + "consecutive_failures": { + "title": "consecutive_failures", + "type": "integer", + "format": "int32" + }, + "created_at": { + "title": "created_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "created_by": { + "title": "created_by", + "type": "string" + }, + "day_of_week": { + "title": "day_of_week", + "type": "integer", + "format": "int32" + }, + "failure_threshold": { + "title": "failure_threshold", + "type": "integer", + "format": "int32" + }, + "hour": { + "title": "hour", + "type": "integer", + "format": "int32" + }, + "id": { + "title": "id", + "type": "string" + }, + "last_error_code": { + "title": "last_error_code", + "type": "string" + }, + "last_error_message": { + "title": "last_error_message", + "type": "string" + }, + "last_run_at": { + "title": "last_run_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "minute": { + "title": "minute", + "type": "integer", + "format": "int32" + }, + "next_run_at": { + "title": "next_run_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "project_id": { + "title": "project_id", + "type": "string", + "format": "int64" + }, + "repository_id": { + "title": "repository_id", + "type": "string" + }, + "status": { + "title": "status", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_SYNC_SCHEDULE_STATUS_UNSPECIFIED - \n * 1 - REPOSITORY_SYNC_SCHEDULE_STATUS_ENABLED - \n * 2 - REPOSITORY_SYNC_SCHEDULE_STATUS_PAUSED - \n * 3 - REPOSITORY_SYNC_SCHEDULE_STATUS_AUTO_PAUSED - \n", + "enum": [ + 0, + 1, + 2, + 3 + ] + }, + "timezone": { + "title": "timezone", + "type": "string" + }, + "updated_at": { + "title": "updated_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + } + }, + "title": "trpc.ocmanager.repository.RepositorySyncSchedule", + "description": "RepositorySyncSchedule" + }, + "trpc.ocmanager.repository.RepositorySyncScheduleResponse": { + "type": "object", + "properties": { + "schedule": { + "title": "schedule", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositorySyncSchedule" + } + }, + "title": "trpc.ocmanager.repository.RepositorySyncScheduleResponse", + "description": "RepositorySyncScheduleResponse" + }, + "trpc.ocmanager.repository.RepositorySyncScheduleSpec": { + "type": "object", + "properties": { + "cadence": { + "title": "cadence", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_SYNC_SCHEDULE_CADENCE_UNSPECIFIED - \n * 1 - REPOSITORY_SYNC_SCHEDULE_CADENCE_DAILY - \n * 2 - REPOSITORY_SYNC_SCHEDULE_CADENCE_WEEKLY - \n", + "enum": [ + 0, + 1, + 2 + ] + }, + "day_of_week": { + "title": "day_of_week", + "type": "integer", + "format": "int32" + }, + "failure_threshold": { + "title": "failure_threshold", + "type": "integer", + "format": "int32" + }, + "hour": { + "title": "hour", + "type": "integer", + "format": "int32" + }, + "minute": { + "title": "minute", + "type": "integer", + "format": "int32" + }, + "timezone": { + "title": "timezone", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositorySyncScheduleSpec", + "description": "RepositorySyncScheduleSpec" + }, + "trpc.ocmanager.repository.RepositoryTask": { + "type": "object", + "properties": { + "activated_at": { + "title": "activated_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "attempt": { + "title": "attempt", + "type": "integer", + "format": "int32" + }, + "available_at": { + "title": "available_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "bandwidth_limit_bytes": { + "title": "bandwidth_limit_bytes", + "type": "string", + "format": "int64" + }, + "cancel_requested_at": { + "title": "cancel_requested_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "commit_state": { + "title": "commit_state", + "type": "string" + }, + "commit_token": { + "title": "commit_token", + "type": "string" + }, + "confirmed_at": { + "title": "confirmed_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "created_at": { + "title": "created_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "current_step": { + "title": "current_step", + "type": "string" + }, + "diagnosis": { + "title": "diagnosis", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryDiagnosis" + }, + "error_code": { + "title": "error_code", + "type": "string" + }, + "error_message": { + "title": "error_message", + "type": "string" + }, + "error_zh_message": { + "title": "error_zh_message", + "type": "string" + }, + "error_zh_recommended_action": { + "title": "error_zh_recommended_action", + "type": "string" + }, + "executor_kind": { + "title": "executor_kind", + "type": "string" + }, + "fencing_token": { + "title": "fencing_token", + "type": "string", + "format": "uint64" + }, + "finished_at": { + "title": "finished_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "id": { + "title": "id", + "type": "string" + }, + "idempotency_key": { + "title": "idempotency_key", + "type": "string" + }, + "last_heartbeat_at": { + "title": "last_heartbeat_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "lease_expires_at": { + "title": "lease_expires_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "max_attempts": { + "title": "max_attempts", + "type": "integer", + "format": "int32" + }, + "max_parallel_downloads": { + "title": "max_parallel_downloads", + "type": "integer", + "format": "int32" + }, + "preflight_id": { + "title": "preflight_id", + "type": "string" + }, + "prepared_at": { + "title": "prepared_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "prepared_manifest_sha256": { + "title": "prepared_manifest_sha256", + "type": "string" + }, + "prepared_revision_id": { + "title": "prepared_revision_id", + "type": "string" + }, + "progress_percent": { + "title": "progress_percent", + "type": "integer", + "format": "int32" + }, + "project_id": { + "title": "project_id", + "type": "string", + "format": "int64" + }, + "repository_id": { + "title": "repository_id", + "type": "string" + }, + "repository_name": { + "title": "repository_name", + "type": "string" + }, + "requested_by": { + "title": "requested_by", + "type": "string" + }, + "requires_mutation_lock": { + "title": "requires_mutation_lock", + "type": "boolean" + }, + "result_items": { + "title": "result_items", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryTaskResultItem" + } + }, + "result_object_id": { + "title": "result_object_id", + "type": "string" + }, + "result_object_sha256": { + "title": "result_object_sha256", + "type": "string" + }, + "retry_of_task_id": { + "title": "retry_of_task_id", + "type": "string" + }, + "revision_id": { + "title": "revision_id", + "type": "string" + }, + "root_task_id": { + "title": "root_task_id", + "type": "string" + }, + "scope_id": { + "title": "scope_id", + "type": "string" + }, + "scope_type": { + "title": "scope_type", + "type": "string" + }, + "started_at": { + "title": "started_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "status": { + "title": "status", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_TASK_STATUS_UNSPECIFIED - \n * 1 - REPOSITORY_TASK_STATUS_PENDING - \n * 2 - REPOSITORY_TASK_STATUS_RUNNING - \n * 3 - REPOSITORY_TASK_STATUS_CANCEL_REQUESTED - \n * 4 - REPOSITORY_TASK_STATUS_CANCELED - \n * 5 - REPOSITORY_TASK_STATUS_SUCCESS - \n * 6 - REPOSITORY_TASK_STATUS_FAILED - \n * 7 - REPOSITORY_TASK_STATUS_PARTIAL_SUCCESS - \n", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ] + }, + "storage_node_id": { + "title": "storage_node_id", + "type": "string" + }, + "storage_node_name": { + "title": "storage_node_name", + "type": "string" + }, + "targets": { + "title": "targets", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryTaskTarget" + } + }, + "trigger": { + "title": "trigger", + "type": "string" + }, + "type": { + "title": "type", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_TASK_TYPE_UNSPECIFIED - \n * 1 - REPOSITORY_TASK_TYPE_SYNC - \n * 2 - REPOSITORY_TASK_TYPE_UPLOAD - \n * 3 - REPOSITORY_TASK_TYPE_DELETE_PACKAGE - \n * 4 - REPOSITORY_TASK_TYPE_REBUILD_METADATA - \n * 5 - REPOSITORY_TASK_TYPE_DELETE_REPOSITORY - \n * 6 - REPOSITORY_TASK_TYPE_PUBLISH - \n * 7 - REPOSITORY_TASK_TYPE_VALIDATE - \n * 8 - REPOSITORY_TASK_TYPE_DISTRIBUTE - \n * 9 - REPOSITORY_TASK_TYPE_MIGRATE_REPOSITORY - \n * 10 - REPOSITORY_TASK_TYPE_NODE_INSTALL - \n * 11 - REPOSITORY_TASK_TYPE_PACKAGE_INSTALL - \n * 12 - REPOSITORY_TASK_TYPE_INVENTORY_IMPORT - \n", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12 + ] + }, + "updated_at": { + "title": "updated_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "worker_id": { + "title": "worker_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryTask", + "description": "RepositoryTask" + }, + "trpc.ocmanager.repository.RepositoryTaskLog": { + "type": "object", + "properties": { + "compressed_bytes": { + "title": "compressed_bytes", + "type": "string", + "format": "int64" + }, + "created_at": { + "title": "created_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "error_fingerprint": { + "title": "error_fingerprint", + "type": "string" + }, + "expires_at": { + "title": "expires_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "line_count": { + "title": "line_count", + "type": "string", + "format": "int64" + }, + "sha256": { + "title": "sha256", + "type": "string" + }, + "storage_node_id": { + "title": "storage_node_id", + "type": "string" + }, + "summary": { + "title": "summary", + "type": "string" + }, + "task_id": { + "title": "task_id", + "type": "string" + }, + "uncompressed_bytes": { + "title": "uncompressed_bytes", + "type": "string", + "format": "int64" + } + }, + "title": "trpc.ocmanager.repository.RepositoryTaskLog", + "description": "RepositoryTaskLog" + }, + "trpc.ocmanager.repository.RepositoryTaskLogResponse": { + "type": "object", + "properties": { + "task_log": { + "title": "task_log", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryTaskLog" + } + }, + "title": "trpc.ocmanager.repository.RepositoryTaskLogResponse", + "description": "RepositoryTaskLogResponse" + }, + "trpc.ocmanager.repository.RepositoryTaskResponse": { + "type": "object", + "properties": { + "task": { + "title": "task", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryTask" + } + }, + "title": "trpc.ocmanager.repository.RepositoryTaskResponse", + "description": "RepositoryTaskResponse" + }, + "trpc.ocmanager.repository.RepositoryTaskResultItem": { + "type": "object", + "properties": { + "created_at": { + "title": "created_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "error_code": { + "title": "error_code", + "type": "string" + }, + "finished_at": { + "title": "finished_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "id": { + "title": "id", + "type": "string" + }, + "item_key": { + "title": "item_key", + "type": "string" + }, + "message": { + "title": "message", + "type": "string" + }, + "package_nevra": { + "title": "package_nevra", + "type": "string" + }, + "result_json": { + "title": "result_json", + "type": "string" + }, + "started_at": { + "title": "started_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "status": { + "title": "status", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_TASK_STATUS_UNSPECIFIED - \n * 1 - REPOSITORY_TASK_STATUS_PENDING - \n * 2 - REPOSITORY_TASK_STATUS_RUNNING - \n * 3 - REPOSITORY_TASK_STATUS_CANCEL_REQUESTED - \n * 4 - REPOSITORY_TASK_STATUS_CANCELED - \n * 5 - REPOSITORY_TASK_STATUS_SUCCESS - \n * 6 - REPOSITORY_TASK_STATUS_FAILED - \n * 7 - REPOSITORY_TASK_STATUS_PARTIAL_SUCCESS - \n", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ] + }, + "task_id": { + "title": "task_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryTaskResultItem", + "description": "RepositoryTaskResultItem" + }, + "trpc.ocmanager.repository.RepositoryTaskStatistics": { + "type": "object", + "properties": { + "failed_count": { + "title": "failed_count", + "type": "string", + "format": "int64" + }, + "pending_count": { + "title": "pending_count", + "type": "string", + "format": "int64" + }, + "running_count": { + "title": "running_count", + "type": "string", + "format": "int64" + }, + "success_count": { + "title": "success_count", + "type": "string", + "format": "int64" + } + }, + "title": "trpc.ocmanager.repository.RepositoryTaskStatistics", + "description": "RepositoryTaskStatistics" + }, + "trpc.ocmanager.repository.RepositoryTaskTarget": { + "type": "object", + "properties": { + "created_at": { + "title": "created_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "error_code": { + "title": "error_code", + "type": "string" + }, + "error_message": { + "title": "error_message", + "type": "string" + }, + "error_zh_message": { + "title": "error_zh_message", + "type": "string" + }, + "error_zh_recommended_action": { + "title": "error_zh_recommended_action", + "type": "string" + }, + "finished_at": { + "title": "finished_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "id": { + "title": "id", + "type": "string" + }, + "progress_percent": { + "title": "progress_percent", + "type": "integer", + "format": "int32" + }, + "result_json": { + "title": "result_json", + "type": "string" + }, + "started_at": { + "title": "started_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "status": { + "title": "status", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_TASK_STATUS_UNSPECIFIED - \n * 1 - REPOSITORY_TASK_STATUS_PENDING - \n * 2 - REPOSITORY_TASK_STATUS_RUNNING - \n * 3 - REPOSITORY_TASK_STATUS_CANCEL_REQUESTED - \n * 4 - REPOSITORY_TASK_STATUS_CANCELED - \n * 5 - REPOSITORY_TASK_STATUS_SUCCESS - \n * 6 - REPOSITORY_TASK_STATUS_FAILED - \n * 7 - REPOSITORY_TASK_STATUS_PARTIAL_SUCCESS - \n", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ] + }, + "storage_node_id": { + "title": "storage_node_id", + "type": "string" + }, + "target_id": { + "title": "target_id", + "type": "string" + }, + "target_type": { + "title": "target_type", + "type": "string" + }, + "task_id": { + "title": "task_id", + "type": "string" + }, + "tms_agent_id": { + "title": "tms_agent_id", + "type": "string", + "format": "uint64" + }, + "updated_at": { + "title": "updated_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + } + }, + "title": "trpc.ocmanager.repository.RepositoryTaskTarget", + "description": "RepositoryTaskTarget" + }, + "trpc.ocmanager.repository.RepositoryUpload": { + "type": "object", + "properties": { + "actual_sha256": { + "title": "actual_sha256", + "type": "string" + }, + "chunk_size": { + "title": "chunk_size", + "type": "integer", + "format": "int32" + }, + "client_fingerprint": { + "title": "client_fingerprint", + "type": "string" + }, + "completed_at": { + "title": "completed_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "created_at": { + "title": "created_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "error_code": { + "title": "error_code", + "type": "string" + }, + "error_message": { + "title": "error_message", + "type": "string" + }, + "expected_sha256": { + "title": "expected_sha256", + "type": "string" + }, + "expected_size": { + "title": "expected_size", + "type": "string", + "format": "int64" + }, + "expires_at": { + "title": "expires_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "file_name": { + "title": "file_name", + "type": "string" + }, + "id": { + "title": "id", + "type": "string" + }, + "kind": { + "title": "kind", + "type": "string" + }, + "paused_at": { + "title": "paused_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "project_id": { + "title": "project_id", + "type": "string", + "format": "int64" + }, + "received_size": { + "title": "received_size", + "type": "string", + "format": "int64" + }, + "repository_id": { + "title": "repository_id", + "type": "string" + }, + "status": { + "title": "status", + "type": "string" + }, + "storage_node_id": { + "title": "storage_node_id", + "type": "string" + }, + "task_id": { + "title": "task_id", + "type": "string" + }, + "total_chunks": { + "title": "total_chunks", + "type": "integer", + "format": "int32" + }, + "updated_at": { + "title": "updated_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "uploaded_count": { + "title": "uploaded_count", + "type": "integer", + "format": "int32" + } + }, + "title": "trpc.ocmanager.repository.RepositoryUpload", + "description": "RepositoryUpload" + }, + "trpc.ocmanager.repository.RepositoryUploadChunk": { + "type": "object", + "properties": { + "chunk_index": { + "title": "chunk_index", + "type": "integer", + "format": "int32" + }, + "created_at": { + "title": "created_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "sha256": { + "title": "sha256", + "type": "string" + }, + "size_bytes": { + "title": "size_bytes", + "type": "integer", + "format": "int32" + } + }, + "title": "trpc.ocmanager.repository.RepositoryUploadChunk", + "description": "RepositoryUploadChunk" + }, + "trpc.ocmanager.repository.RepositoryUploadResponse": { + "type": "object", + "properties": { + "upload": { + "title": "upload", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryUpload" + } + }, + "title": "trpc.ocmanager.repository.RepositoryUploadResponse", + "description": "RepositoryUploadResponse" + }, + "trpc.ocmanager.repository.RepositoryUpstreamProfile": { + "type": "object", + "properties": { + "channel": { + "title": "channel", + "type": "string" + }, + "created_at": { + "title": "created_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "created_by": { + "title": "created_by", + "type": "string" + }, + "default_exclude_packages": { + "title": "default_exclude_packages", + "type": "array", + "items": { + "type": "string" + } + }, + "default_include_packages": { + "title": "default_include_packages", + "type": "array", + "items": { + "type": "string" + } + }, + "description": { + "title": "description", + "type": "string" + }, + "enabled": { + "title": "enabled", + "type": "boolean" + }, + "id": { + "title": "id", + "type": "string" + }, + "name": { + "title": "name", + "type": "string" + }, + "project_id": { + "title": "project_id", + "type": "string", + "format": "int64" + }, + "supported_architectures": { + "title": "supported_architectures", + "type": "array", + "format": "int32", + "description": " * 0 - REPOSITORY_ARCHITECTURE_UNSPECIFIED - \n * 1 - REPOSITORY_ARCHITECTURE_X86_64 - \n * 2 - REPOSITORY_ARCHITECTURE_AARCH64 - \n * 3 - REPOSITORY_ARCHITECTURE_NOARCH - \n", + "enum": [ + 0, + 1, + 2, + 3 + ], + "items": { + "type": "integer", + "format": "int32" + } + }, + "supported_versions": { + "title": "supported_versions", + "type": "array", + "items": { + "type": "string" + } + }, + "system_product": { + "title": "system_product", + "type": "string" + }, + "updated_at": { + "title": "updated_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "url_template": { + "title": "url_template", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryUpstreamProfile", + "description": "RepositoryUpstreamProfile" + }, + "trpc.ocmanager.repository.RepositoryUpstreamProfileResponse": { + "type": "object", + "properties": { + "profile": { + "title": "profile", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryUpstreamProfile" + } + }, + "title": "trpc.ocmanager.repository.RepositoryUpstreamProfileResponse", + "description": "RepositoryUpstreamProfileResponse" + }, + "trpc.ocmanager.repository.RepositoryUpstreamProfileSpec": { + "type": "object", + "properties": { + "channel": { + "title": "channel", + "type": "string" + }, + "default_exclude_packages": { + "title": "default_exclude_packages", + "type": "array", + "items": { + "type": "string" + } + }, + "default_include_packages": { + "title": "default_include_packages", + "type": "array", + "items": { + "type": "string" + } + }, + "description": { + "title": "description", + "type": "string" + }, + "enabled": { + "title": "enabled", + "type": "boolean" + }, + "name": { + "title": "name", + "type": "string" + }, + "project_id": { + "title": "project_id", + "type": "string", + "format": "int64" + }, + "supported_architectures": { + "title": "supported_architectures", + "type": "array", + "format": "int32", + "description": " * 0 - REPOSITORY_ARCHITECTURE_UNSPECIFIED - \n * 1 - REPOSITORY_ARCHITECTURE_X86_64 - \n * 2 - REPOSITORY_ARCHITECTURE_AARCH64 - \n * 3 - REPOSITORY_ARCHITECTURE_NOARCH - \n", + "enum": [ + 0, + 1, + 2, + 3 + ], + "items": { + "type": "integer", + "format": "int32" + } + }, + "supported_versions": { + "title": "supported_versions", + "type": "array", + "items": { + "type": "string" + } + }, + "system_product": { + "title": "system_product", + "type": "string" + }, + "url_template": { + "title": "url_template", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RepositoryUpstreamProfileSpec", + "description": "Upstream profiles are reusable form templates. Repository creation resolves\n them into the existing RepositorySpec fields and does not persist a profile\n dependency in the repository runtime model." + }, + "trpc.ocmanager.repository.RetryRepositoryTaskRequest": { + "type": "object", + "properties": { + "task_id": { + "title": "task_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.RetryRepositoryTaskRequest", + "description": "RetryRepositoryTaskRequest" + }, + "trpc.ocmanager.repository.SyncRepositoryRequest": { + "type": "object", + "properties": { + "repository_id": { + "title": "repository_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.SyncRepositoryRequest", + "description": "SyncRepositoryRequest" + }, + "trpc.ocmanager.repository.UpdateRepositoryRequest": { + "type": "object", + "properties": { + "repository_id": { + "title": "repository_id", + "type": "string" + }, + "spec": { + "title": "spec", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositorySpec" + } + }, + "title": "trpc.ocmanager.repository.UpdateRepositoryRequest", + "description": "UpdateRepositoryRequest" + }, + "trpc.ocmanager.repository.UpdateRepositoryStorageNodeRequest": { + "type": "object", + "properties": { + "spec": { + "title": "spec", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryStorageNodeSpec" + }, + "storage_node_id": { + "title": "storage_node_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.UpdateRepositoryStorageNodeRequest", + "description": "UpdateRepositoryStorageNodeRequest" + }, + "trpc.ocmanager.repository.UpdateRepositoryUpstreamProfileRequest": { + "type": "object", + "properties": { + "spec": { + "title": "spec", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryUpstreamProfileSpec" + }, + "upstream_profile_id": { + "title": "upstream_profile_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repository.UpdateRepositoryUpstreamProfileRequest", + "description": "UpdateRepositoryUpstreamProfileRequest" + }, + "trpc.ocmanager.repository.UpsertRepositoryNodeRegistrationAllowlistRequest": { + "type": "object", + "properties": { + "spec": { + "title": "spec", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositoryNodeRegistrationAllowlistSpec" + } + }, + "title": "trpc.ocmanager.repository.UpsertRepositoryNodeRegistrationAllowlistRequest", + "description": "UpsertRepositoryNodeRegistrationAllowlistRequest" + }, + "trpc.ocmanager.repository.UpsertRepositorySyncScheduleRequest": { + "type": "object", + "properties": { + "repository_id": { + "title": "repository_id", + "type": "string" + }, + "spec": { + "title": "spec", + "$ref": "#/definitions/trpc.ocmanager.repository.RepositorySyncScheduleSpec" + } + }, + "title": "trpc.ocmanager.repository.UpsertRepositorySyncScheduleRequest", + "description": "UpsertRepositorySyncScheduleRequest" + } + } +} \ No newline at end of file diff --git a/manager/backend/protocol/repository/build.sh b/manager/backend/protocol/repository/build.sh new file mode 100644 index 0000000000000000000000000000000000000000..09d3fa3016ae6ab04797e07cd545abf6871674b2 --- /dev/null +++ b/manager/backend/protocol/repository/build.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -euo pipefail + +for tool in trpc mockgen; do + if ! command -v "${tool}" >/dev/null 2>&1; then + echo "ERROR: '${tool}' not found in PATH" >&2 + exit 1 + fi +done + +candidates=( + "${HOME}/.trpc-cmdline-assets/submodules/trpc-protocol/trpc/validate" + "${HOME}/.trpc-cmdline-assets/validate" +) +validate_dir="" +for candidate in "${candidates[@]}"; do + if [[ -f "${candidate}/validate.proto" ]]; then + validate_dir="${candidate}" + break + fi +done +if [[ -z "${validate_dir}" ]]; then + echo "ERROR: validate.proto not found in trpc-cmdline assets" >&2 + exit 1 +fi +if grep -q 'git\.code\.oa\.com\|git\.woa\.com' "${validate_dir}/validate.proto"; then + echo "ERROR: validate.proto uses an internal go_package" >&2 + exit 1 +fi + +protocol_root=$(cd "$(dirname "$0")/.." && pwd) +backend_root=$(cd "${protocol_root}/.." && pwd) +cd "${protocol_root}" +trpc create --nogomod \ + --protofile=repository/repository.proto \ + --rpconly=true \ + --protodir=. --protodir="${validate_dir}" \ + --domain=trpc.group \ + --output=repository --goversion=1.18 \ + --validate=true --mock=false \ + --swagger --swagger-json-param --usebasename=false +rm -f repository/go.mod repository/go.sum +cd "${backend_root}" +mockgen -source=protocol/repository/repository.trpc.go \ + -destination=protocol/repository/repository_mock.go -package=repository diff --git a/manager/backend/protocol/repository/repository.pb.go b/manager/backend/protocol/repository/repository.pb.go new file mode 100644 index 0000000000000000000000000000000000000000..22ae56b14f4f6b5a1e2d4f155e256b2013495178 --- /dev/null +++ b/manager/backend/protocol/repository/repository.pb.go @@ -0,0 +1,19736 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.26.0 +// protoc v4.24.2 +// source: repository/repository.proto + +package repository + +import ( + reflect "reflect" + sync "sync" + + common "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/common" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + _ "trpc.group/trpc/trpc-protocol/pb/go/trpc/validate" +) + +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 RepositoryType int32 + +const ( + RepositoryType_REPOSITORY_TYPE_UNSPECIFIED RepositoryType = 0 + RepositoryType_REPOSITORY_TYPE_BASELINE RepositoryType = 1 + RepositoryType_REPOSITORY_TYPE_UPDATE RepositoryType = 2 + RepositoryType_REPOSITORY_TYPE_PRIVATE RepositoryType = 3 +) + +// Enum value maps for RepositoryType. +var ( + RepositoryType_name = map[int32]string{ + 0: "REPOSITORY_TYPE_UNSPECIFIED", + 1: "REPOSITORY_TYPE_BASELINE", + 2: "REPOSITORY_TYPE_UPDATE", + 3: "REPOSITORY_TYPE_PRIVATE", + } + RepositoryType_value = map[string]int32{ + "REPOSITORY_TYPE_UNSPECIFIED": 0, + "REPOSITORY_TYPE_BASELINE": 1, + "REPOSITORY_TYPE_UPDATE": 2, + "REPOSITORY_TYPE_PRIVATE": 3, + } +) + +func (x RepositoryType) Enum() *RepositoryType { + p := new(RepositoryType) + *p = x + return p +} + +func (x RepositoryType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (RepositoryType) Descriptor() protoreflect.EnumDescriptor { + return file_repository_repository_proto_enumTypes[0].Descriptor() +} + +func (RepositoryType) Type() protoreflect.EnumType { + return &file_repository_repository_proto_enumTypes[0] +} + +func (x RepositoryType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use RepositoryType.Descriptor instead. +func (RepositoryType) EnumDescriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{0} +} + +type RepositoryArchitecture int32 + +const ( + RepositoryArchitecture_REPOSITORY_ARCHITECTURE_UNSPECIFIED RepositoryArchitecture = 0 + RepositoryArchitecture_REPOSITORY_ARCHITECTURE_X86_64 RepositoryArchitecture = 1 + RepositoryArchitecture_REPOSITORY_ARCHITECTURE_AARCH64 RepositoryArchitecture = 2 + RepositoryArchitecture_REPOSITORY_ARCHITECTURE_NOARCH RepositoryArchitecture = 3 +) + +// Enum value maps for RepositoryArchitecture. +var ( + RepositoryArchitecture_name = map[int32]string{ + 0: "REPOSITORY_ARCHITECTURE_UNSPECIFIED", + 1: "REPOSITORY_ARCHITECTURE_X86_64", + 2: "REPOSITORY_ARCHITECTURE_AARCH64", + 3: "REPOSITORY_ARCHITECTURE_NOARCH", + } + RepositoryArchitecture_value = map[string]int32{ + "REPOSITORY_ARCHITECTURE_UNSPECIFIED": 0, + "REPOSITORY_ARCHITECTURE_X86_64": 1, + "REPOSITORY_ARCHITECTURE_AARCH64": 2, + "REPOSITORY_ARCHITECTURE_NOARCH": 3, + } +) + +func (x RepositoryArchitecture) Enum() *RepositoryArchitecture { + p := new(RepositoryArchitecture) + *p = x + return p +} + +func (x RepositoryArchitecture) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (RepositoryArchitecture) Descriptor() protoreflect.EnumDescriptor { + return file_repository_repository_proto_enumTypes[1].Descriptor() +} + +func (RepositoryArchitecture) Type() protoreflect.EnumType { + return &file_repository_repository_proto_enumTypes[1] +} + +func (x RepositoryArchitecture) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use RepositoryArchitecture.Descriptor instead. +func (RepositoryArchitecture) EnumDescriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{1} +} + +type RepositoryGPGPolicy int32 + +const ( + RepositoryGPGPolicy_REPOSITORY_GPG_POLICY_UNSPECIFIED RepositoryGPGPolicy = 0 + RepositoryGPGPolicy_REPOSITORY_GPG_POLICY_DISABLED RepositoryGPGPolicy = 1 + // Deprecated compatibility value. The behavior is identical to ENFORCE. + RepositoryGPGPolicy_REPOSITORY_GPG_POLICY_VERIFY_PACKAGE RepositoryGPGPolicy = 2 + RepositoryGPGPolicy_REPOSITORY_GPG_POLICY_AUDIT RepositoryGPGPolicy = 3 + RepositoryGPGPolicy_REPOSITORY_GPG_POLICY_ENFORCE RepositoryGPGPolicy = 4 +) + +// Enum value maps for RepositoryGPGPolicy. +var ( + RepositoryGPGPolicy_name = map[int32]string{ + 0: "REPOSITORY_GPG_POLICY_UNSPECIFIED", + 1: "REPOSITORY_GPG_POLICY_DISABLED", + 2: "REPOSITORY_GPG_POLICY_VERIFY_PACKAGE", + 3: "REPOSITORY_GPG_POLICY_AUDIT", + 4: "REPOSITORY_GPG_POLICY_ENFORCE", + } + RepositoryGPGPolicy_value = map[string]int32{ + "REPOSITORY_GPG_POLICY_UNSPECIFIED": 0, + "REPOSITORY_GPG_POLICY_DISABLED": 1, + "REPOSITORY_GPG_POLICY_VERIFY_PACKAGE": 2, + "REPOSITORY_GPG_POLICY_AUDIT": 3, + "REPOSITORY_GPG_POLICY_ENFORCE": 4, + } +) + +func (x RepositoryGPGPolicy) Enum() *RepositoryGPGPolicy { + p := new(RepositoryGPGPolicy) + *p = x + return p +} + +func (x RepositoryGPGPolicy) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (RepositoryGPGPolicy) Descriptor() protoreflect.EnumDescriptor { + return file_repository_repository_proto_enumTypes[2].Descriptor() +} + +func (RepositoryGPGPolicy) Type() protoreflect.EnumType { + return &file_repository_repository_proto_enumTypes[2] +} + +func (x RepositoryGPGPolicy) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use RepositoryGPGPolicy.Descriptor instead. +func (RepositoryGPGPolicy) EnumDescriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{2} +} + +type RepositorySyncMode int32 + +const ( + RepositorySyncMode_REPOSITORY_SYNC_MODE_UNSPECIFIED RepositorySyncMode = 0 + RepositorySyncMode_REPOSITORY_SYNC_MODE_MIRROR RepositorySyncMode = 1 + RepositorySyncMode_REPOSITORY_SYNC_MODE_APPEND RepositorySyncMode = 2 +) + +// Enum value maps for RepositorySyncMode. +var ( + RepositorySyncMode_name = map[int32]string{ + 0: "REPOSITORY_SYNC_MODE_UNSPECIFIED", + 1: "REPOSITORY_SYNC_MODE_MIRROR", + 2: "REPOSITORY_SYNC_MODE_APPEND", + } + RepositorySyncMode_value = map[string]int32{ + "REPOSITORY_SYNC_MODE_UNSPECIFIED": 0, + "REPOSITORY_SYNC_MODE_MIRROR": 1, + "REPOSITORY_SYNC_MODE_APPEND": 2, + } +) + +func (x RepositorySyncMode) Enum() *RepositorySyncMode { + p := new(RepositorySyncMode) + *p = x + return p +} + +func (x RepositorySyncMode) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (RepositorySyncMode) Descriptor() protoreflect.EnumDescriptor { + return file_repository_repository_proto_enumTypes[3].Descriptor() +} + +func (RepositorySyncMode) Type() protoreflect.EnumType { + return &file_repository_repository_proto_enumTypes[3] +} + +func (x RepositorySyncMode) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use RepositorySyncMode.Descriptor instead. +func (RepositorySyncMode) EnumDescriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{3} +} + +type RepositoryInventoryConflictStrategy int32 + +const ( + RepositoryInventoryConflictStrategy_REPOSITORY_INVENTORY_CONFLICT_STRATEGY_UNSPECIFIED RepositoryInventoryConflictStrategy = 0 + RepositoryInventoryConflictStrategy_REPOSITORY_INVENTORY_CONFLICT_STRATEGY_FAIL RepositoryInventoryConflictStrategy = 1 + RepositoryInventoryConflictStrategy_REPOSITORY_INVENTORY_CONFLICT_STRATEGY_SKIP RepositoryInventoryConflictStrategy = 2 + RepositoryInventoryConflictStrategy_REPOSITORY_INVENTORY_CONFLICT_STRATEGY_RENAME RepositoryInventoryConflictStrategy = 3 +) + +// Enum value maps for RepositoryInventoryConflictStrategy. +var ( + RepositoryInventoryConflictStrategy_name = map[int32]string{ + 0: "REPOSITORY_INVENTORY_CONFLICT_STRATEGY_UNSPECIFIED", + 1: "REPOSITORY_INVENTORY_CONFLICT_STRATEGY_FAIL", + 2: "REPOSITORY_INVENTORY_CONFLICT_STRATEGY_SKIP", + 3: "REPOSITORY_INVENTORY_CONFLICT_STRATEGY_RENAME", + } + RepositoryInventoryConflictStrategy_value = map[string]int32{ + "REPOSITORY_INVENTORY_CONFLICT_STRATEGY_UNSPECIFIED": 0, + "REPOSITORY_INVENTORY_CONFLICT_STRATEGY_FAIL": 1, + "REPOSITORY_INVENTORY_CONFLICT_STRATEGY_SKIP": 2, + "REPOSITORY_INVENTORY_CONFLICT_STRATEGY_RENAME": 3, + } +) + +func (x RepositoryInventoryConflictStrategy) Enum() *RepositoryInventoryConflictStrategy { + p := new(RepositoryInventoryConflictStrategy) + *p = x + return p +} + +func (x RepositoryInventoryConflictStrategy) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (RepositoryInventoryConflictStrategy) Descriptor() protoreflect.EnumDescriptor { + return file_repository_repository_proto_enumTypes[4].Descriptor() +} + +func (RepositoryInventoryConflictStrategy) Type() protoreflect.EnumType { + return &file_repository_repository_proto_enumTypes[4] +} + +func (x RepositoryInventoryConflictStrategy) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use RepositoryInventoryConflictStrategy.Descriptor instead. +func (RepositoryInventoryConflictStrategy) EnumDescriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{4} +} + +type RepositorySyncScheduleCadence int32 + +const ( + RepositorySyncScheduleCadence_REPOSITORY_SYNC_SCHEDULE_CADENCE_UNSPECIFIED RepositorySyncScheduleCadence = 0 + RepositorySyncScheduleCadence_REPOSITORY_SYNC_SCHEDULE_CADENCE_DAILY RepositorySyncScheduleCadence = 1 + RepositorySyncScheduleCadence_REPOSITORY_SYNC_SCHEDULE_CADENCE_WEEKLY RepositorySyncScheduleCadence = 2 +) + +// Enum value maps for RepositorySyncScheduleCadence. +var ( + RepositorySyncScheduleCadence_name = map[int32]string{ + 0: "REPOSITORY_SYNC_SCHEDULE_CADENCE_UNSPECIFIED", + 1: "REPOSITORY_SYNC_SCHEDULE_CADENCE_DAILY", + 2: "REPOSITORY_SYNC_SCHEDULE_CADENCE_WEEKLY", + } + RepositorySyncScheduleCadence_value = map[string]int32{ + "REPOSITORY_SYNC_SCHEDULE_CADENCE_UNSPECIFIED": 0, + "REPOSITORY_SYNC_SCHEDULE_CADENCE_DAILY": 1, + "REPOSITORY_SYNC_SCHEDULE_CADENCE_WEEKLY": 2, + } +) + +func (x RepositorySyncScheduleCadence) Enum() *RepositorySyncScheduleCadence { + p := new(RepositorySyncScheduleCadence) + *p = x + return p +} + +func (x RepositorySyncScheduleCadence) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (RepositorySyncScheduleCadence) Descriptor() protoreflect.EnumDescriptor { + return file_repository_repository_proto_enumTypes[5].Descriptor() +} + +func (RepositorySyncScheduleCadence) Type() protoreflect.EnumType { + return &file_repository_repository_proto_enumTypes[5] +} + +func (x RepositorySyncScheduleCadence) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use RepositorySyncScheduleCadence.Descriptor instead. +func (RepositorySyncScheduleCadence) EnumDescriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{5} +} + +type RepositorySyncScheduleStatus int32 + +const ( + RepositorySyncScheduleStatus_REPOSITORY_SYNC_SCHEDULE_STATUS_UNSPECIFIED RepositorySyncScheduleStatus = 0 + RepositorySyncScheduleStatus_REPOSITORY_SYNC_SCHEDULE_STATUS_ENABLED RepositorySyncScheduleStatus = 1 + RepositorySyncScheduleStatus_REPOSITORY_SYNC_SCHEDULE_STATUS_PAUSED RepositorySyncScheduleStatus = 2 + RepositorySyncScheduleStatus_REPOSITORY_SYNC_SCHEDULE_STATUS_AUTO_PAUSED RepositorySyncScheduleStatus = 3 +) + +// Enum value maps for RepositorySyncScheduleStatus. +var ( + RepositorySyncScheduleStatus_name = map[int32]string{ + 0: "REPOSITORY_SYNC_SCHEDULE_STATUS_UNSPECIFIED", + 1: "REPOSITORY_SYNC_SCHEDULE_STATUS_ENABLED", + 2: "REPOSITORY_SYNC_SCHEDULE_STATUS_PAUSED", + 3: "REPOSITORY_SYNC_SCHEDULE_STATUS_AUTO_PAUSED", + } + RepositorySyncScheduleStatus_value = map[string]int32{ + "REPOSITORY_SYNC_SCHEDULE_STATUS_UNSPECIFIED": 0, + "REPOSITORY_SYNC_SCHEDULE_STATUS_ENABLED": 1, + "REPOSITORY_SYNC_SCHEDULE_STATUS_PAUSED": 2, + "REPOSITORY_SYNC_SCHEDULE_STATUS_AUTO_PAUSED": 3, + } +) + +func (x RepositorySyncScheduleStatus) Enum() *RepositorySyncScheduleStatus { + p := new(RepositorySyncScheduleStatus) + *p = x + return p +} + +func (x RepositorySyncScheduleStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (RepositorySyncScheduleStatus) Descriptor() protoreflect.EnumDescriptor { + return file_repository_repository_proto_enumTypes[6].Descriptor() +} + +func (RepositorySyncScheduleStatus) Type() protoreflect.EnumType { + return &file_repository_repository_proto_enumTypes[6] +} + +func (x RepositorySyncScheduleStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use RepositorySyncScheduleStatus.Descriptor instead. +func (RepositorySyncScheduleStatus) EnumDescriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{6} +} + +type RepositoryTaskType int32 + +const ( + RepositoryTaskType_REPOSITORY_TASK_TYPE_UNSPECIFIED RepositoryTaskType = 0 + RepositoryTaskType_REPOSITORY_TASK_TYPE_SYNC RepositoryTaskType = 1 + RepositoryTaskType_REPOSITORY_TASK_TYPE_UPLOAD RepositoryTaskType = 2 + RepositoryTaskType_REPOSITORY_TASK_TYPE_DELETE_PACKAGE RepositoryTaskType = 3 + RepositoryTaskType_REPOSITORY_TASK_TYPE_REBUILD_METADATA RepositoryTaskType = 4 + RepositoryTaskType_REPOSITORY_TASK_TYPE_DELETE_REPOSITORY RepositoryTaskType = 5 + RepositoryTaskType_REPOSITORY_TASK_TYPE_PUBLISH RepositoryTaskType = 6 + RepositoryTaskType_REPOSITORY_TASK_TYPE_VALIDATE RepositoryTaskType = 7 + RepositoryTaskType_REPOSITORY_TASK_TYPE_DISTRIBUTE RepositoryTaskType = 8 + RepositoryTaskType_REPOSITORY_TASK_TYPE_MIGRATE_REPOSITORY RepositoryTaskType = 9 + RepositoryTaskType_REPOSITORY_TASK_TYPE_NODE_INSTALL RepositoryTaskType = 10 + RepositoryTaskType_REPOSITORY_TASK_TYPE_PACKAGE_INSTALL RepositoryTaskType = 11 + RepositoryTaskType_REPOSITORY_TASK_TYPE_INVENTORY_IMPORT RepositoryTaskType = 12 +) + +// Enum value maps for RepositoryTaskType. +var ( + RepositoryTaskType_name = map[int32]string{ + 0: "REPOSITORY_TASK_TYPE_UNSPECIFIED", + 1: "REPOSITORY_TASK_TYPE_SYNC", + 2: "REPOSITORY_TASK_TYPE_UPLOAD", + 3: "REPOSITORY_TASK_TYPE_DELETE_PACKAGE", + 4: "REPOSITORY_TASK_TYPE_REBUILD_METADATA", + 5: "REPOSITORY_TASK_TYPE_DELETE_REPOSITORY", + 6: "REPOSITORY_TASK_TYPE_PUBLISH", + 7: "REPOSITORY_TASK_TYPE_VALIDATE", + 8: "REPOSITORY_TASK_TYPE_DISTRIBUTE", + 9: "REPOSITORY_TASK_TYPE_MIGRATE_REPOSITORY", + 10: "REPOSITORY_TASK_TYPE_NODE_INSTALL", + 11: "REPOSITORY_TASK_TYPE_PACKAGE_INSTALL", + 12: "REPOSITORY_TASK_TYPE_INVENTORY_IMPORT", + } + RepositoryTaskType_value = map[string]int32{ + "REPOSITORY_TASK_TYPE_UNSPECIFIED": 0, + "REPOSITORY_TASK_TYPE_SYNC": 1, + "REPOSITORY_TASK_TYPE_UPLOAD": 2, + "REPOSITORY_TASK_TYPE_DELETE_PACKAGE": 3, + "REPOSITORY_TASK_TYPE_REBUILD_METADATA": 4, + "REPOSITORY_TASK_TYPE_DELETE_REPOSITORY": 5, + "REPOSITORY_TASK_TYPE_PUBLISH": 6, + "REPOSITORY_TASK_TYPE_VALIDATE": 7, + "REPOSITORY_TASK_TYPE_DISTRIBUTE": 8, + "REPOSITORY_TASK_TYPE_MIGRATE_REPOSITORY": 9, + "REPOSITORY_TASK_TYPE_NODE_INSTALL": 10, + "REPOSITORY_TASK_TYPE_PACKAGE_INSTALL": 11, + "REPOSITORY_TASK_TYPE_INVENTORY_IMPORT": 12, + } +) + +func (x RepositoryTaskType) Enum() *RepositoryTaskType { + p := new(RepositoryTaskType) + *p = x + return p +} + +func (x RepositoryTaskType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (RepositoryTaskType) Descriptor() protoreflect.EnumDescriptor { + return file_repository_repository_proto_enumTypes[7].Descriptor() +} + +func (RepositoryTaskType) Type() protoreflect.EnumType { + return &file_repository_repository_proto_enumTypes[7] +} + +func (x RepositoryTaskType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use RepositoryTaskType.Descriptor instead. +func (RepositoryTaskType) EnumDescriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{7} +} + +type RepositoryTaskStatus int32 + +const ( + RepositoryTaskStatus_REPOSITORY_TASK_STATUS_UNSPECIFIED RepositoryTaskStatus = 0 + RepositoryTaskStatus_REPOSITORY_TASK_STATUS_PENDING RepositoryTaskStatus = 1 + RepositoryTaskStatus_REPOSITORY_TASK_STATUS_RUNNING RepositoryTaskStatus = 2 + RepositoryTaskStatus_REPOSITORY_TASK_STATUS_CANCEL_REQUESTED RepositoryTaskStatus = 3 + RepositoryTaskStatus_REPOSITORY_TASK_STATUS_CANCELED RepositoryTaskStatus = 4 + RepositoryTaskStatus_REPOSITORY_TASK_STATUS_SUCCESS RepositoryTaskStatus = 5 + RepositoryTaskStatus_REPOSITORY_TASK_STATUS_FAILED RepositoryTaskStatus = 6 + RepositoryTaskStatus_REPOSITORY_TASK_STATUS_PARTIAL_SUCCESS RepositoryTaskStatus = 7 +) + +// Enum value maps for RepositoryTaskStatus. +var ( + RepositoryTaskStatus_name = map[int32]string{ + 0: "REPOSITORY_TASK_STATUS_UNSPECIFIED", + 1: "REPOSITORY_TASK_STATUS_PENDING", + 2: "REPOSITORY_TASK_STATUS_RUNNING", + 3: "REPOSITORY_TASK_STATUS_CANCEL_REQUESTED", + 4: "REPOSITORY_TASK_STATUS_CANCELED", + 5: "REPOSITORY_TASK_STATUS_SUCCESS", + 6: "REPOSITORY_TASK_STATUS_FAILED", + 7: "REPOSITORY_TASK_STATUS_PARTIAL_SUCCESS", + } + RepositoryTaskStatus_value = map[string]int32{ + "REPOSITORY_TASK_STATUS_UNSPECIFIED": 0, + "REPOSITORY_TASK_STATUS_PENDING": 1, + "REPOSITORY_TASK_STATUS_RUNNING": 2, + "REPOSITORY_TASK_STATUS_CANCEL_REQUESTED": 3, + "REPOSITORY_TASK_STATUS_CANCELED": 4, + "REPOSITORY_TASK_STATUS_SUCCESS": 5, + "REPOSITORY_TASK_STATUS_FAILED": 6, + "REPOSITORY_TASK_STATUS_PARTIAL_SUCCESS": 7, + } +) + +func (x RepositoryTaskStatus) Enum() *RepositoryTaskStatus { + p := new(RepositoryTaskStatus) + *p = x + return p +} + +func (x RepositoryTaskStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (RepositoryTaskStatus) Descriptor() protoreflect.EnumDescriptor { + return file_repository_repository_proto_enumTypes[8].Descriptor() +} + +func (RepositoryTaskStatus) Type() protoreflect.EnumType { + return &file_repository_repository_proto_enumTypes[8] +} + +func (x RepositoryTaskStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use RepositoryTaskStatus.Descriptor instead. +func (RepositoryTaskStatus) EnumDescriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{8} +} + +type RepositoryStorageNodeStatus int32 + +const ( + RepositoryStorageNodeStatus_REPOSITORY_STORAGE_NODE_STATUS_UNSPECIFIED RepositoryStorageNodeStatus = 0 + RepositoryStorageNodeStatus_REPOSITORY_STORAGE_NODE_STATUS_UNKNOWN RepositoryStorageNodeStatus = 1 + RepositoryStorageNodeStatus_REPOSITORY_STORAGE_NODE_STATUS_ONLINE RepositoryStorageNodeStatus = 2 + RepositoryStorageNodeStatus_REPOSITORY_STORAGE_NODE_STATUS_OFFLINE RepositoryStorageNodeStatus = 3 + RepositoryStorageNodeStatus_REPOSITORY_STORAGE_NODE_STATUS_DISABLED RepositoryStorageNodeStatus = 4 + RepositoryStorageNodeStatus_REPOSITORY_STORAGE_NODE_STATUS_PENDING RepositoryStorageNodeStatus = 5 + RepositoryStorageNodeStatus_REPOSITORY_STORAGE_NODE_STATUS_INSTALLING RepositoryStorageNodeStatus = 6 + RepositoryStorageNodeStatus_REPOSITORY_STORAGE_NODE_STATUS_INSTALL_FAILED RepositoryStorageNodeStatus = 7 +) + +// Enum value maps for RepositoryStorageNodeStatus. +var ( + RepositoryStorageNodeStatus_name = map[int32]string{ + 0: "REPOSITORY_STORAGE_NODE_STATUS_UNSPECIFIED", + 1: "REPOSITORY_STORAGE_NODE_STATUS_UNKNOWN", + 2: "REPOSITORY_STORAGE_NODE_STATUS_ONLINE", + 3: "REPOSITORY_STORAGE_NODE_STATUS_OFFLINE", + 4: "REPOSITORY_STORAGE_NODE_STATUS_DISABLED", + 5: "REPOSITORY_STORAGE_NODE_STATUS_PENDING", + 6: "REPOSITORY_STORAGE_NODE_STATUS_INSTALLING", + 7: "REPOSITORY_STORAGE_NODE_STATUS_INSTALL_FAILED", + } + RepositoryStorageNodeStatus_value = map[string]int32{ + "REPOSITORY_STORAGE_NODE_STATUS_UNSPECIFIED": 0, + "REPOSITORY_STORAGE_NODE_STATUS_UNKNOWN": 1, + "REPOSITORY_STORAGE_NODE_STATUS_ONLINE": 2, + "REPOSITORY_STORAGE_NODE_STATUS_OFFLINE": 3, + "REPOSITORY_STORAGE_NODE_STATUS_DISABLED": 4, + "REPOSITORY_STORAGE_NODE_STATUS_PENDING": 5, + "REPOSITORY_STORAGE_NODE_STATUS_INSTALLING": 6, + "REPOSITORY_STORAGE_NODE_STATUS_INSTALL_FAILED": 7, + } +) + +func (x RepositoryStorageNodeStatus) Enum() *RepositoryStorageNodeStatus { + p := new(RepositoryStorageNodeStatus) + *p = x + return p +} + +func (x RepositoryStorageNodeStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (RepositoryStorageNodeStatus) Descriptor() protoreflect.EnumDescriptor { + return file_repository_repository_proto_enumTypes[9].Descriptor() +} + +func (RepositoryStorageNodeStatus) Type() protoreflect.EnumType { + return &file_repository_repository_proto_enumTypes[9] +} + +func (x RepositoryStorageNodeStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use RepositoryStorageNodeStatus.Descriptor instead. +func (RepositoryStorageNodeStatus) EnumDescriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{9} +} + +type RepositoryNodeEnrollmentStatus int32 + +const ( + RepositoryNodeEnrollmentStatus_REPOSITORY_NODE_ENROLLMENT_STATUS_UNSPECIFIED RepositoryNodeEnrollmentStatus = 0 + RepositoryNodeEnrollmentStatus_REPOSITORY_NODE_ENROLLMENT_STATUS_PENDING RepositoryNodeEnrollmentStatus = 1 + RepositoryNodeEnrollmentStatus_REPOSITORY_NODE_ENROLLMENT_STATUS_INSTALLING RepositoryNodeEnrollmentStatus = 2 + RepositoryNodeEnrollmentStatus_REPOSITORY_NODE_ENROLLMENT_STATUS_BOOTSTRAPPED RepositoryNodeEnrollmentStatus = 3 + RepositoryNodeEnrollmentStatus_REPOSITORY_NODE_ENROLLMENT_STATUS_ONLINE RepositoryNodeEnrollmentStatus = 4 + RepositoryNodeEnrollmentStatus_REPOSITORY_NODE_ENROLLMENT_STATUS_FAILED RepositoryNodeEnrollmentStatus = 5 + RepositoryNodeEnrollmentStatus_REPOSITORY_NODE_ENROLLMENT_STATUS_EXPIRED RepositoryNodeEnrollmentStatus = 6 + RepositoryNodeEnrollmentStatus_REPOSITORY_NODE_ENROLLMENT_STATUS_AUTHORIZED RepositoryNodeEnrollmentStatus = 7 + RepositoryNodeEnrollmentStatus_REPOSITORY_NODE_ENROLLMENT_STATUS_REGISTERING RepositoryNodeEnrollmentStatus = 8 + RepositoryNodeEnrollmentStatus_REPOSITORY_NODE_ENROLLMENT_STATUS_REGISTERED RepositoryNodeEnrollmentStatus = 9 +) + +// Enum value maps for RepositoryNodeEnrollmentStatus. +var ( + RepositoryNodeEnrollmentStatus_name = map[int32]string{ + 0: "REPOSITORY_NODE_ENROLLMENT_STATUS_UNSPECIFIED", + 1: "REPOSITORY_NODE_ENROLLMENT_STATUS_PENDING", + 2: "REPOSITORY_NODE_ENROLLMENT_STATUS_INSTALLING", + 3: "REPOSITORY_NODE_ENROLLMENT_STATUS_BOOTSTRAPPED", + 4: "REPOSITORY_NODE_ENROLLMENT_STATUS_ONLINE", + 5: "REPOSITORY_NODE_ENROLLMENT_STATUS_FAILED", + 6: "REPOSITORY_NODE_ENROLLMENT_STATUS_EXPIRED", + 7: "REPOSITORY_NODE_ENROLLMENT_STATUS_AUTHORIZED", + 8: "REPOSITORY_NODE_ENROLLMENT_STATUS_REGISTERING", + 9: "REPOSITORY_NODE_ENROLLMENT_STATUS_REGISTERED", + } + RepositoryNodeEnrollmentStatus_value = map[string]int32{ + "REPOSITORY_NODE_ENROLLMENT_STATUS_UNSPECIFIED": 0, + "REPOSITORY_NODE_ENROLLMENT_STATUS_PENDING": 1, + "REPOSITORY_NODE_ENROLLMENT_STATUS_INSTALLING": 2, + "REPOSITORY_NODE_ENROLLMENT_STATUS_BOOTSTRAPPED": 3, + "REPOSITORY_NODE_ENROLLMENT_STATUS_ONLINE": 4, + "REPOSITORY_NODE_ENROLLMENT_STATUS_FAILED": 5, + "REPOSITORY_NODE_ENROLLMENT_STATUS_EXPIRED": 6, + "REPOSITORY_NODE_ENROLLMENT_STATUS_AUTHORIZED": 7, + "REPOSITORY_NODE_ENROLLMENT_STATUS_REGISTERING": 8, + "REPOSITORY_NODE_ENROLLMENT_STATUS_REGISTERED": 9, + } +) + +func (x RepositoryNodeEnrollmentStatus) Enum() *RepositoryNodeEnrollmentStatus { + p := new(RepositoryNodeEnrollmentStatus) + *p = x + return p +} + +func (x RepositoryNodeEnrollmentStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (RepositoryNodeEnrollmentStatus) Descriptor() protoreflect.EnumDescriptor { + return file_repository_repository_proto_enumTypes[10].Descriptor() +} + +func (RepositoryNodeEnrollmentStatus) Type() protoreflect.EnumType { + return &file_repository_repository_proto_enumTypes[10] +} + +func (x RepositoryNodeEnrollmentStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use RepositoryNodeEnrollmentStatus.Descriptor instead. +func (RepositoryNodeEnrollmentStatus) EnumDescriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{10} +} + +type RepositorySpec struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ProjectId int64 `protobuf:"varint,1,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + SystemProduct string `protobuf:"bytes,2,opt,name=system_product,json=systemProduct,proto3" json:"system_product,omitempty"` + ProductVersion string `protobuf:"bytes,3,opt,name=product_version,json=productVersion,proto3" json:"product_version,omitempty"` + Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty"` + Architecture RepositoryArchitecture `protobuf:"varint,6,opt,name=architecture,proto3,enum=trpc.ocmanager.repository.RepositoryArchitecture" json:"architecture,omitempty"` + Type RepositoryType `protobuf:"varint,7,opt,name=type,proto3,enum=trpc.ocmanager.repository.RepositoryType" json:"type,omitempty"` + UpstreamUrl string `protobuf:"bytes,8,opt,name=upstream_url,json=upstreamUrl,proto3" json:"upstream_url,omitempty"` + AllowUpload bool `protobuf:"varint,9,opt,name=allow_upload,json=allowUpload,proto3" json:"allow_upload,omitempty"` + IncludePackages []string `protobuf:"bytes,10,rep,name=include_packages,json=includePackages,proto3" json:"include_packages,omitempty"` + ExcludePackages []string `protobuf:"bytes,11,rep,name=exclude_packages,json=excludePackages,proto3" json:"exclude_packages,omitempty"` + GpgPolicy RepositoryGPGPolicy `protobuf:"varint,12,opt,name=gpg_policy,json=gpgPolicy,proto3,enum=trpc.ocmanager.repository.RepositoryGPGPolicy" json:"gpg_policy,omitempty"` + Enabled *bool `protobuf:"varint,13,opt,name=enabled,proto3,oneof" json:"enabled,omitempty"` + PrimaryStorageNodeId string `protobuf:"bytes,14,opt,name=primary_storage_node_id,json=primaryStorageNodeId,proto3" json:"primary_storage_node_id,omitempty"` + SyncMode RepositorySyncMode `protobuf:"varint,15,opt,name=sync_mode,json=syncMode,proto3,enum=trpc.ocmanager.repository.RepositorySyncMode" json:"sync_mode,omitempty"` + BandwidthLimitBytes int64 `protobuf:"varint,16,opt,name=bandwidth_limit_bytes,json=bandwidthLimitBytes,proto3" json:"bandwidth_limit_bytes,omitempty"` + MaxParallelDownloads int32 `protobuf:"varint,17,opt,name=max_parallel_downloads,json=maxParallelDownloads,proto3" json:"max_parallel_downloads,omitempty"` + MetadataSigningKeyRef string `protobuf:"bytes,18,opt,name=metadata_signing_key_ref,json=metadataSigningKeyRef,proto3" json:"metadata_signing_key_ref,omitempty"` +} + +func (x *RepositorySpec) Reset() { + *x = RepositorySpec{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositorySpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositorySpec) ProtoMessage() {} + +func (x *RepositorySpec) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositorySpec.ProtoReflect.Descriptor instead. +func (*RepositorySpec) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{0} +} + +func (x *RepositorySpec) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +func (x *RepositorySpec) GetSystemProduct() string { + if x != nil { + return x.SystemProduct + } + return "" +} + +func (x *RepositorySpec) GetProductVersion() string { + if x != nil { + return x.ProductVersion + } + return "" +} + +func (x *RepositorySpec) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *RepositorySpec) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *RepositorySpec) GetArchitecture() RepositoryArchitecture { + if x != nil { + return x.Architecture + } + return RepositoryArchitecture_REPOSITORY_ARCHITECTURE_UNSPECIFIED +} + +func (x *RepositorySpec) GetType() RepositoryType { + if x != nil { + return x.Type + } + return RepositoryType_REPOSITORY_TYPE_UNSPECIFIED +} + +func (x *RepositorySpec) GetUpstreamUrl() string { + if x != nil { + return x.UpstreamUrl + } + return "" +} + +func (x *RepositorySpec) GetAllowUpload() bool { + if x != nil { + return x.AllowUpload + } + return false +} + +func (x *RepositorySpec) GetIncludePackages() []string { + if x != nil { + return x.IncludePackages + } + return nil +} + +func (x *RepositorySpec) GetExcludePackages() []string { + if x != nil { + return x.ExcludePackages + } + return nil +} + +func (x *RepositorySpec) GetGpgPolicy() RepositoryGPGPolicy { + if x != nil { + return x.GpgPolicy + } + return RepositoryGPGPolicy_REPOSITORY_GPG_POLICY_UNSPECIFIED +} + +func (x *RepositorySpec) GetEnabled() bool { + if x != nil && x.Enabled != nil { + return *x.Enabled + } + return false +} + +func (x *RepositorySpec) GetPrimaryStorageNodeId() string { + if x != nil { + return x.PrimaryStorageNodeId + } + return "" +} + +func (x *RepositorySpec) GetSyncMode() RepositorySyncMode { + if x != nil { + return x.SyncMode + } + return RepositorySyncMode_REPOSITORY_SYNC_MODE_UNSPECIFIED +} + +func (x *RepositorySpec) GetBandwidthLimitBytes() int64 { + if x != nil { + return x.BandwidthLimitBytes + } + return 0 +} + +func (x *RepositorySpec) GetMaxParallelDownloads() int32 { + if x != nil { + return x.MaxParallelDownloads + } + return 0 +} + +func (x *RepositorySpec) GetMetadataSigningKeyRef() string { + if x != nil { + return x.MetadataSigningKeyRef + } + return "" +} + +type Repository struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + ProjectId int64 `protobuf:"varint,2,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + SystemProduct string `protobuf:"bytes,3,opt,name=system_product,json=systemProduct,proto3" json:"system_product,omitempty"` + ProductVersion string `protobuf:"bytes,4,opt,name=product_version,json=productVersion,proto3" json:"product_version,omitempty"` + Name string `protobuf:"bytes,5,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,6,opt,name=description,proto3" json:"description,omitempty"` + Architecture RepositoryArchitecture `protobuf:"varint,7,opt,name=architecture,proto3,enum=trpc.ocmanager.repository.RepositoryArchitecture" json:"architecture,omitempty"` + Type RepositoryType `protobuf:"varint,8,opt,name=type,proto3,enum=trpc.ocmanager.repository.RepositoryType" json:"type,omitempty"` + UpstreamUrl string `protobuf:"bytes,9,opt,name=upstream_url,json=upstreamUrl,proto3" json:"upstream_url,omitempty"` + StorageLocation string `protobuf:"bytes,10,opt,name=storage_location,json=storageLocation,proto3" json:"storage_location,omitempty"` + PublishKey string `protobuf:"bytes,11,opt,name=publish_key,json=publishKey,proto3" json:"publish_key,omitempty"` + AllowUpload bool `protobuf:"varint,12,opt,name=allow_upload,json=allowUpload,proto3" json:"allow_upload,omitempty"` + IncludePackages []string `protobuf:"bytes,13,rep,name=include_packages,json=includePackages,proto3" json:"include_packages,omitempty"` + ExcludePackages []string `protobuf:"bytes,14,rep,name=exclude_packages,json=excludePackages,proto3" json:"exclude_packages,omitempty"` + GpgPolicy RepositoryGPGPolicy `protobuf:"varint,15,opt,name=gpg_policy,json=gpgPolicy,proto3,enum=trpc.ocmanager.repository.RepositoryGPGPolicy" json:"gpg_policy,omitempty"` + Enabled bool `protobuf:"varint,16,opt,name=enabled,proto3" json:"enabled,omitempty"` + Status string `protobuf:"bytes,17,opt,name=status,proto3" json:"status,omitempty"` + ActiveRevisionId string `protobuf:"bytes,18,opt,name=active_revision_id,json=activeRevisionId,proto3" json:"active_revision_id,omitempty"` + Creator string `protobuf:"bytes,19,opt,name=creator,proto3" json:"creator,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,20,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,21,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` + PrimaryStorageNodeId string `protobuf:"bytes,22,opt,name=primary_storage_node_id,json=primaryStorageNodeId,proto3" json:"primary_storage_node_id,omitempty"` + PublishBaseUrlSnapshot string `protobuf:"bytes,23,opt,name=publish_base_url_snapshot,json=publishBaseUrlSnapshot,proto3" json:"publish_base_url_snapshot,omitempty"` + PackageCount int64 `protobuf:"varint,24,opt,name=package_count,json=packageCount,proto3" json:"package_count,omitempty"` + LogicalBytes int64 `protobuf:"varint,25,opt,name=logical_bytes,json=logicalBytes,proto3" json:"logical_bytes,omitempty"` + LastSyncAt *timestamppb.Timestamp `protobuf:"bytes,26,opt,name=last_sync_at,json=lastSyncAt,proto3" json:"last_sync_at,omitempty"` + LastSyncStatus RepositoryTaskStatus `protobuf:"varint,27,opt,name=last_sync_status,json=lastSyncStatus,proto3,enum=trpc.ocmanager.repository.RepositoryTaskStatus" json:"last_sync_status,omitempty"` + SyncMode RepositorySyncMode `protobuf:"varint,28,opt,name=sync_mode,json=syncMode,proto3,enum=trpc.ocmanager.repository.RepositorySyncMode" json:"sync_mode,omitempty"` + BandwidthLimitBytes int64 `protobuf:"varint,29,opt,name=bandwidth_limit_bytes,json=bandwidthLimitBytes,proto3" json:"bandwidth_limit_bytes,omitempty"` + MaxParallelDownloads int32 `protobuf:"varint,30,opt,name=max_parallel_downloads,json=maxParallelDownloads,proto3" json:"max_parallel_downloads,omitempty"` + MetadataSigningKeyRef string `protobuf:"bytes,31,opt,name=metadata_signing_key_ref,json=metadataSigningKeyRef,proto3" json:"metadata_signing_key_ref,omitempty"` + RevisionCount int64 `protobuf:"varint,32,opt,name=revision_count,json=revisionCount,proto3" json:"revision_count,omitempty"` +} + +func (x *Repository) Reset() { + *x = Repository{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Repository) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Repository) ProtoMessage() {} + +func (x *Repository) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Repository.ProtoReflect.Descriptor instead. +func (*Repository) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{1} +} + +func (x *Repository) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Repository) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +func (x *Repository) GetSystemProduct() string { + if x != nil { + return x.SystemProduct + } + return "" +} + +func (x *Repository) GetProductVersion() string { + if x != nil { + return x.ProductVersion + } + return "" +} + +func (x *Repository) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Repository) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Repository) GetArchitecture() RepositoryArchitecture { + if x != nil { + return x.Architecture + } + return RepositoryArchitecture_REPOSITORY_ARCHITECTURE_UNSPECIFIED +} + +func (x *Repository) GetType() RepositoryType { + if x != nil { + return x.Type + } + return RepositoryType_REPOSITORY_TYPE_UNSPECIFIED +} + +func (x *Repository) GetUpstreamUrl() string { + if x != nil { + return x.UpstreamUrl + } + return "" +} + +func (x *Repository) GetStorageLocation() string { + if x != nil { + return x.StorageLocation + } + return "" +} + +func (x *Repository) GetPublishKey() string { + if x != nil { + return x.PublishKey + } + return "" +} + +func (x *Repository) GetAllowUpload() bool { + if x != nil { + return x.AllowUpload + } + return false +} + +func (x *Repository) GetIncludePackages() []string { + if x != nil { + return x.IncludePackages + } + return nil +} + +func (x *Repository) GetExcludePackages() []string { + if x != nil { + return x.ExcludePackages + } + return nil +} + +func (x *Repository) GetGpgPolicy() RepositoryGPGPolicy { + if x != nil { + return x.GpgPolicy + } + return RepositoryGPGPolicy_REPOSITORY_GPG_POLICY_UNSPECIFIED +} + +func (x *Repository) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +func (x *Repository) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *Repository) GetActiveRevisionId() string { + if x != nil { + return x.ActiveRevisionId + } + return "" +} + +func (x *Repository) GetCreator() string { + if x != nil { + return x.Creator + } + return "" +} + +func (x *Repository) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *Repository) GetUpdatedAt() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedAt + } + return nil +} + +func (x *Repository) GetPrimaryStorageNodeId() string { + if x != nil { + return x.PrimaryStorageNodeId + } + return "" +} + +func (x *Repository) GetPublishBaseUrlSnapshot() string { + if x != nil { + return x.PublishBaseUrlSnapshot + } + return "" +} + +func (x *Repository) GetPackageCount() int64 { + if x != nil { + return x.PackageCount + } + return 0 +} + +func (x *Repository) GetLogicalBytes() int64 { + if x != nil { + return x.LogicalBytes + } + return 0 +} + +func (x *Repository) GetLastSyncAt() *timestamppb.Timestamp { + if x != nil { + return x.LastSyncAt + } + return nil +} + +func (x *Repository) GetLastSyncStatus() RepositoryTaskStatus { + if x != nil { + return x.LastSyncStatus + } + return RepositoryTaskStatus_REPOSITORY_TASK_STATUS_UNSPECIFIED +} + +func (x *Repository) GetSyncMode() RepositorySyncMode { + if x != nil { + return x.SyncMode + } + return RepositorySyncMode_REPOSITORY_SYNC_MODE_UNSPECIFIED +} + +func (x *Repository) GetBandwidthLimitBytes() int64 { + if x != nil { + return x.BandwidthLimitBytes + } + return 0 +} + +func (x *Repository) GetMaxParallelDownloads() int32 { + if x != nil { + return x.MaxParallelDownloads + } + return 0 +} + +func (x *Repository) GetMetadataSigningKeyRef() string { + if x != nil { + return x.MetadataSigningKeyRef + } + return "" +} + +func (x *Repository) GetRevisionCount() int64 { + if x != nil { + return x.RevisionCount + } + return 0 +} + +type CreateRepositoryRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Spec *RepositorySpec `protobuf:"bytes,1,opt,name=spec,proto3" json:"spec,omitempty"` +} + +func (x *CreateRepositoryRequest) Reset() { + *x = CreateRepositoryRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateRepositoryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateRepositoryRequest) ProtoMessage() {} + +func (x *CreateRepositoryRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateRepositoryRequest.ProtoReflect.Descriptor instead. +func (*CreateRepositoryRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{2} +} + +func (x *CreateRepositoryRequest) GetSpec() *RepositorySpec { + if x != nil { + return x.Spec + } + return nil +} + +type UpdateRepositoryRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepositoryId string `protobuf:"bytes,1,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + Spec *RepositorySpec `protobuf:"bytes,2,opt,name=spec,proto3" json:"spec,omitempty"` +} + +func (x *UpdateRepositoryRequest) Reset() { + *x = UpdateRepositoryRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UpdateRepositoryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateRepositoryRequest) ProtoMessage() {} + +func (x *UpdateRepositoryRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateRepositoryRequest.ProtoReflect.Descriptor instead. +func (*UpdateRepositoryRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{3} +} + +func (x *UpdateRepositoryRequest) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *UpdateRepositoryRequest) GetSpec() *RepositorySpec { + if x != nil { + return x.Spec + } + return nil +} + +type DescribeRepositoryRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepositoryId string `protobuf:"bytes,1,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` +} + +func (x *DescribeRepositoryRequest) Reset() { + *x = DescribeRepositoryRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DescribeRepositoryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeRepositoryRequest) ProtoMessage() {} + +func (x *DescribeRepositoryRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeRepositoryRequest.ProtoReflect.Descriptor instead. +func (*DescribeRepositoryRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{4} +} + +func (x *DescribeRepositoryRequest) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +type ListRepositoriesRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ProjectId int64 `protobuf:"varint,1,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + SystemProduct string `protobuf:"bytes,2,opt,name=system_product,json=systemProduct,proto3" json:"system_product,omitempty"` + ProductVersion string `protobuf:"bytes,3,opt,name=product_version,json=productVersion,proto3" json:"product_version,omitempty"` + Architecture RepositoryArchitecture `protobuf:"varint,4,opt,name=architecture,proto3,enum=trpc.ocmanager.repository.RepositoryArchitecture" json:"architecture,omitempty"` + Type RepositoryType `protobuf:"varint,5,opt,name=type,proto3,enum=trpc.ocmanager.repository.RepositoryType" json:"type,omitempty"` + Keyword string `protobuf:"bytes,6,opt,name=keyword,proto3" json:"keyword,omitempty"` + PageRequest *common.PageRequest `protobuf:"bytes,10,opt,name=page_request,json=pageRequest,proto3" json:"page_request,omitempty"` +} + +func (x *ListRepositoriesRequest) Reset() { + *x = ListRepositoriesRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListRepositoriesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRepositoriesRequest) ProtoMessage() {} + +func (x *ListRepositoriesRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRepositoriesRequest.ProtoReflect.Descriptor instead. +func (*ListRepositoriesRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{5} +} + +func (x *ListRepositoriesRequest) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +func (x *ListRepositoriesRequest) GetSystemProduct() string { + if x != nil { + return x.SystemProduct + } + return "" +} + +func (x *ListRepositoriesRequest) GetProductVersion() string { + if x != nil { + return x.ProductVersion + } + return "" +} + +func (x *ListRepositoriesRequest) GetArchitecture() RepositoryArchitecture { + if x != nil { + return x.Architecture + } + return RepositoryArchitecture_REPOSITORY_ARCHITECTURE_UNSPECIFIED +} + +func (x *ListRepositoriesRequest) GetType() RepositoryType { + if x != nil { + return x.Type + } + return RepositoryType_REPOSITORY_TYPE_UNSPECIFIED +} + +func (x *ListRepositoriesRequest) GetKeyword() string { + if x != nil { + return x.Keyword + } + return "" +} + +func (x *ListRepositoriesRequest) GetPageRequest() *common.PageRequest { + if x != nil { + return x.PageRequest + } + return nil +} + +type DeleteRepositoryRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepositoryId string `protobuf:"bytes,1,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + ConfirmDelete bool `protobuf:"varint,2,opt,name=confirm_delete,json=confirmDelete,proto3" json:"confirm_delete,omitempty"` +} + +func (x *DeleteRepositoryRequest) Reset() { + *x = DeleteRepositoryRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteRepositoryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteRepositoryRequest) ProtoMessage() {} + +func (x *DeleteRepositoryRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteRepositoryRequest.ProtoReflect.Descriptor instead. +func (*DeleteRepositoryRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{6} +} + +func (x *DeleteRepositoryRequest) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *DeleteRepositoryRequest) GetConfirmDelete() bool { + if x != nil { + return x.ConfirmDelete + } + return false +} + +type BatchApplyRepositoryItem struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Empty repository_id creates a repository; a non-empty ID updates it. + RepositoryId string `protobuf:"bytes,1,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + Spec *RepositorySpec `protobuf:"bytes,2,opt,name=spec,proto3" json:"spec,omitempty"` +} + +func (x *BatchApplyRepositoryItem) Reset() { + *x = BatchApplyRepositoryItem{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *BatchApplyRepositoryItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BatchApplyRepositoryItem) ProtoMessage() {} + +func (x *BatchApplyRepositoryItem) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BatchApplyRepositoryItem.ProtoReflect.Descriptor instead. +func (*BatchApplyRepositoryItem) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{7} +} + +func (x *BatchApplyRepositoryItem) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *BatchApplyRepositoryItem) GetSpec() *RepositorySpec { + if x != nil { + return x.Spec + } + return nil +} + +type BatchApplyRepositoriesRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Items []*BatchApplyRepositoryItem `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` +} + +func (x *BatchApplyRepositoriesRequest) Reset() { + *x = BatchApplyRepositoriesRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *BatchApplyRepositoriesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BatchApplyRepositoriesRequest) ProtoMessage() {} + +func (x *BatchApplyRepositoriesRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BatchApplyRepositoriesRequest.ProtoReflect.Descriptor instead. +func (*BatchApplyRepositoriesRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{8} +} + +func (x *BatchApplyRepositoriesRequest) GetItems() []*BatchApplyRepositoryItem { + if x != nil { + return x.Items + } + return nil +} + +type BatchApplyRepositoryResult struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + InputIndex int32 `protobuf:"varint,1,opt,name=input_index,json=inputIndex,proto3" json:"input_index,omitempty"` + Operation string `protobuf:"bytes,2,opt,name=operation,proto3" json:"operation,omitempty"` + Repository *Repository `protobuf:"bytes,3,opt,name=repository,proto3" json:"repository,omitempty"` +} + +func (x *BatchApplyRepositoryResult) Reset() { + *x = BatchApplyRepositoryResult{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *BatchApplyRepositoryResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BatchApplyRepositoryResult) ProtoMessage() {} + +func (x *BatchApplyRepositoryResult) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BatchApplyRepositoryResult.ProtoReflect.Descriptor instead. +func (*BatchApplyRepositoryResult) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{9} +} + +func (x *BatchApplyRepositoryResult) GetInputIndex() int32 { + if x != nil { + return x.InputIndex + } + return 0 +} + +func (x *BatchApplyRepositoryResult) GetOperation() string { + if x != nil { + return x.Operation + } + return "" +} + +func (x *BatchApplyRepositoryResult) GetRepository() *Repository { + if x != nil { + return x.Repository + } + return nil +} + +type RepositoryResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Repository *Repository `protobuf:"bytes,1,opt,name=repository,proto3" json:"repository,omitempty"` +} + +func (x *RepositoryResponse) Reset() { + *x = RepositoryResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryResponse) ProtoMessage() {} + +func (x *RepositoryResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[10] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryResponse.ProtoReflect.Descriptor instead. +func (*RepositoryResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{10} +} + +func (x *RepositoryResponse) GetRepository() *Repository { + if x != nil { + return x.Repository + } + return nil +} + +type ListRepositoriesResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Repositories []*Repository `protobuf:"bytes,1,rep,name=repositories,proto3" json:"repositories,omitempty"` + PageResponse *common.PageResponse `protobuf:"bytes,10,opt,name=page_response,json=pageResponse,proto3" json:"page_response,omitempty"` +} + +func (x *ListRepositoriesResponse) Reset() { + *x = ListRepositoriesResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListRepositoriesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRepositoriesResponse) ProtoMessage() {} + +func (x *ListRepositoriesResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[11] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRepositoriesResponse.ProtoReflect.Descriptor instead. +func (*ListRepositoriesResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{11} +} + +func (x *ListRepositoriesResponse) GetRepositories() []*Repository { + if x != nil { + return x.Repositories + } + return nil +} + +func (x *ListRepositoriesResponse) GetPageResponse() *common.PageResponse { + if x != nil { + return x.PageResponse + } + return nil +} + +type BatchApplyRepositoriesResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Results []*BatchApplyRepositoryResult `protobuf:"bytes,1,rep,name=results,proto3" json:"results,omitempty"` +} + +func (x *BatchApplyRepositoriesResponse) Reset() { + *x = BatchApplyRepositoriesResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *BatchApplyRepositoriesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BatchApplyRepositoriesResponse) ProtoMessage() {} + +func (x *BatchApplyRepositoriesResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[12] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BatchApplyRepositoriesResponse.ProtoReflect.Descriptor instead. +func (*BatchApplyRepositoriesResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{12} +} + +func (x *BatchApplyRepositoriesResponse) GetResults() []*BatchApplyRepositoryResult { + if x != nil { + return x.Results + } + return nil +} + +type RepositoryStorageNodeSpec struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ProjectId int64 `protobuf:"varint,1,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + ControlBaseUrl string `protobuf:"bytes,4,opt,name=control_base_url,json=controlBaseUrl,proto3" json:"control_base_url,omitempty"` + PublishBaseUrl string `protobuf:"bytes,5,opt,name=publish_base_url,json=publishBaseUrl,proto3" json:"publish_base_url,omitempty"` + TlsServerName string `protobuf:"bytes,6,opt,name=tls_server_name,json=tlsServerName,proto3" json:"tls_server_name,omitempty"` + CertificateFingerprint string `protobuf:"bytes,7,opt,name=certificate_fingerprint,json=certificateFingerprint,proto3" json:"certificate_fingerprint,omitempty"` + Enabled *bool `protobuf:"varint,8,opt,name=enabled,proto3,oneof" json:"enabled,omitempty"` + Labels map[string]string `protobuf:"bytes,9,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *RepositoryStorageNodeSpec) Reset() { + *x = RepositoryStorageNodeSpec{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryStorageNodeSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryStorageNodeSpec) ProtoMessage() {} + +func (x *RepositoryStorageNodeSpec) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[13] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryStorageNodeSpec.ProtoReflect.Descriptor instead. +func (*RepositoryStorageNodeSpec) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{13} +} + +func (x *RepositoryStorageNodeSpec) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +func (x *RepositoryStorageNodeSpec) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *RepositoryStorageNodeSpec) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *RepositoryStorageNodeSpec) GetControlBaseUrl() string { + if x != nil { + return x.ControlBaseUrl + } + return "" +} + +func (x *RepositoryStorageNodeSpec) GetPublishBaseUrl() string { + if x != nil { + return x.PublishBaseUrl + } + return "" +} + +func (x *RepositoryStorageNodeSpec) GetTlsServerName() string { + if x != nil { + return x.TlsServerName + } + return "" +} + +func (x *RepositoryStorageNodeSpec) GetCertificateFingerprint() string { + if x != nil { + return x.CertificateFingerprint + } + return "" +} + +func (x *RepositoryStorageNodeSpec) GetEnabled() bool { + if x != nil && x.Enabled != nil { + return *x.Enabled + } + return false +} + +func (x *RepositoryStorageNodeSpec) GetLabels() map[string]string { + if x != nil { + return x.Labels + } + return nil +} + +type RepositoryStorageNode struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + ProjectId int64 `protobuf:"varint,2,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` + ControlBaseUrl string `protobuf:"bytes,5,opt,name=control_base_url,json=controlBaseUrl,proto3" json:"control_base_url,omitempty"` + PublishBaseUrl string `protobuf:"bytes,6,opt,name=publish_base_url,json=publishBaseUrl,proto3" json:"publish_base_url,omitempty"` + TlsServerName string `protobuf:"bytes,7,opt,name=tls_server_name,json=tlsServerName,proto3" json:"tls_server_name,omitempty"` + CertificateFingerprint string `protobuf:"bytes,8,opt,name=certificate_fingerprint,json=certificateFingerprint,proto3" json:"certificate_fingerprint,omitempty"` + Status RepositoryStorageNodeStatus `protobuf:"varint,9,opt,name=status,proto3,enum=trpc.ocmanager.repository.RepositoryStorageNodeStatus" json:"status,omitempty"` + Enabled bool `protobuf:"varint,10,opt,name=enabled,proto3" json:"enabled,omitempty"` + FilesystemType string `protobuf:"bytes,11,opt,name=filesystem_type,json=filesystemType,proto3" json:"filesystem_type,omitempty"` + CapacityBytes int64 `protobuf:"varint,12,opt,name=capacity_bytes,json=capacityBytes,proto3" json:"capacity_bytes,omitempty"` + FreeBytes int64 `protobuf:"varint,13,opt,name=free_bytes,json=freeBytes,proto3" json:"free_bytes,omitempty"` + FreeInodes int64 `protobuf:"varint,14,opt,name=free_inodes,json=freeInodes,proto3" json:"free_inodes,omitempty"` + ToolVersions map[string]string `protobuf:"bytes,15,rep,name=tool_versions,json=toolVersions,proto3" json:"tool_versions,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Labels map[string]string `protobuf:"bytes,16,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + LastHeartbeatAt *timestamppb.Timestamp `protobuf:"bytes,17,opt,name=last_heartbeat_at,json=lastHeartbeatAt,proto3" json:"last_heartbeat_at,omitempty"` + LastProbeAt *timestamppb.Timestamp `protobuf:"bytes,18,opt,name=last_probe_at,json=lastProbeAt,proto3" json:"last_probe_at,omitempty"` + LastErrorCode string `protobuf:"bytes,19,opt,name=last_error_code,json=lastErrorCode,proto3" json:"last_error_code,omitempty"` + LastErrorMessage string `protobuf:"bytes,20,opt,name=last_error_message,json=lastErrorMessage,proto3" json:"last_error_message,omitempty"` + CreatedBy string `protobuf:"bytes,21,opt,name=created_by,json=createdBy,proto3" json:"created_by,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,22,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,23,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` + Hostname string `protobuf:"bytes,24,opt,name=hostname,proto3" json:"hostname,omitempty"` + IpAddress string `protobuf:"bytes,25,opt,name=ip_address,json=ipAddress,proto3" json:"ip_address,omitempty"` + Architecture string `protobuf:"bytes,26,opt,name=architecture,proto3" json:"architecture,omitempty"` + OsName string `protobuf:"bytes,27,opt,name=os_name,json=osName,proto3" json:"os_name,omitempty"` + OsVersion string `protobuf:"bytes,28,opt,name=os_version,json=osVersion,proto3" json:"os_version,omitempty"` + AgentVersion string `protobuf:"bytes,29,opt,name=agent_version,json=agentVersion,proto3" json:"agent_version,omitempty"` + DiskUsedBytes int64 `protobuf:"varint,30,opt,name=disk_used_bytes,json=diskUsedBytes,proto3" json:"disk_used_bytes,omitempty"` + DiskUsagePercent float64 `protobuf:"fixed64,31,opt,name=disk_usage_percent,json=diskUsagePercent,proto3" json:"disk_usage_percent,omitempty"` + RepositoryCount int64 `protobuf:"varint,32,opt,name=repository_count,json=repositoryCount,proto3" json:"repository_count,omitempty"` + RepositoryPhysicalBytes int64 `protobuf:"varint,33,opt,name=repository_physical_bytes,json=repositoryPhysicalBytes,proto3" json:"repository_physical_bytes,omitempty"` + ErrorZhMessage string `protobuf:"bytes,34,opt,name=error_zh_message,json=errorZhMessage,proto3" json:"error_zh_message,omitempty"` + ErrorZhRecommendedAction string `protobuf:"bytes,35,opt,name=error_zh_recommended_action,json=errorZhRecommendedAction,proto3" json:"error_zh_recommended_action,omitempty"` +} + +func (x *RepositoryStorageNode) Reset() { + *x = RepositoryStorageNode{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryStorageNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryStorageNode) ProtoMessage() {} + +func (x *RepositoryStorageNode) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[14] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryStorageNode.ProtoReflect.Descriptor instead. +func (*RepositoryStorageNode) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{14} +} + +func (x *RepositoryStorageNode) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *RepositoryStorageNode) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +func (x *RepositoryStorageNode) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *RepositoryStorageNode) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *RepositoryStorageNode) GetControlBaseUrl() string { + if x != nil { + return x.ControlBaseUrl + } + return "" +} + +func (x *RepositoryStorageNode) GetPublishBaseUrl() string { + if x != nil { + return x.PublishBaseUrl + } + return "" +} + +func (x *RepositoryStorageNode) GetTlsServerName() string { + if x != nil { + return x.TlsServerName + } + return "" +} + +func (x *RepositoryStorageNode) GetCertificateFingerprint() string { + if x != nil { + return x.CertificateFingerprint + } + return "" +} + +func (x *RepositoryStorageNode) GetStatus() RepositoryStorageNodeStatus { + if x != nil { + return x.Status + } + return RepositoryStorageNodeStatus_REPOSITORY_STORAGE_NODE_STATUS_UNSPECIFIED +} + +func (x *RepositoryStorageNode) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +func (x *RepositoryStorageNode) GetFilesystemType() string { + if x != nil { + return x.FilesystemType + } + return "" +} + +func (x *RepositoryStorageNode) GetCapacityBytes() int64 { + if x != nil { + return x.CapacityBytes + } + return 0 +} + +func (x *RepositoryStorageNode) GetFreeBytes() int64 { + if x != nil { + return x.FreeBytes + } + return 0 +} + +func (x *RepositoryStorageNode) GetFreeInodes() int64 { + if x != nil { + return x.FreeInodes + } + return 0 +} + +func (x *RepositoryStorageNode) GetToolVersions() map[string]string { + if x != nil { + return x.ToolVersions + } + return nil +} + +func (x *RepositoryStorageNode) GetLabels() map[string]string { + if x != nil { + return x.Labels + } + return nil +} + +func (x *RepositoryStorageNode) GetLastHeartbeatAt() *timestamppb.Timestamp { + if x != nil { + return x.LastHeartbeatAt + } + return nil +} + +func (x *RepositoryStorageNode) GetLastProbeAt() *timestamppb.Timestamp { + if x != nil { + return x.LastProbeAt + } + return nil +} + +func (x *RepositoryStorageNode) GetLastErrorCode() string { + if x != nil { + return x.LastErrorCode + } + return "" +} + +func (x *RepositoryStorageNode) GetLastErrorMessage() string { + if x != nil { + return x.LastErrorMessage + } + return "" +} + +func (x *RepositoryStorageNode) GetCreatedBy() string { + if x != nil { + return x.CreatedBy + } + return "" +} + +func (x *RepositoryStorageNode) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *RepositoryStorageNode) GetUpdatedAt() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedAt + } + return nil +} + +func (x *RepositoryStorageNode) GetHostname() string { + if x != nil { + return x.Hostname + } + return "" +} + +func (x *RepositoryStorageNode) GetIpAddress() string { + if x != nil { + return x.IpAddress + } + return "" +} + +func (x *RepositoryStorageNode) GetArchitecture() string { + if x != nil { + return x.Architecture + } + return "" +} + +func (x *RepositoryStorageNode) GetOsName() string { + if x != nil { + return x.OsName + } + return "" +} + +func (x *RepositoryStorageNode) GetOsVersion() string { + if x != nil { + return x.OsVersion + } + return "" +} + +func (x *RepositoryStorageNode) GetAgentVersion() string { + if x != nil { + return x.AgentVersion + } + return "" +} + +func (x *RepositoryStorageNode) GetDiskUsedBytes() int64 { + if x != nil { + return x.DiskUsedBytes + } + return 0 +} + +func (x *RepositoryStorageNode) GetDiskUsagePercent() float64 { + if x != nil { + return x.DiskUsagePercent + } + return 0 +} + +func (x *RepositoryStorageNode) GetRepositoryCount() int64 { + if x != nil { + return x.RepositoryCount + } + return 0 +} + +func (x *RepositoryStorageNode) GetRepositoryPhysicalBytes() int64 { + if x != nil { + return x.RepositoryPhysicalBytes + } + return 0 +} + +func (x *RepositoryStorageNode) GetErrorZhMessage() string { + if x != nil { + return x.ErrorZhMessage + } + return "" +} + +func (x *RepositoryStorageNode) GetErrorZhRecommendedAction() string { + if x != nil { + return x.ErrorZhRecommendedAction + } + return "" +} + +type RepositoryNodeRepositorySummary struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepositoryId string `protobuf:"bytes,1,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + SystemProduct string `protobuf:"bytes,3,opt,name=system_product,json=systemProduct,proto3" json:"system_product,omitempty"` + ProductVersion string `protobuf:"bytes,4,opt,name=product_version,json=productVersion,proto3" json:"product_version,omitempty"` + RepositoryType RepositoryType `protobuf:"varint,5,opt,name=repository_type,json=repositoryType,proto3,enum=trpc.ocmanager.repository.RepositoryType" json:"repository_type,omitempty"` + Architecture RepositoryArchitecture `protobuf:"varint,6,opt,name=architecture,proto3,enum=trpc.ocmanager.repository.RepositoryArchitecture" json:"architecture,omitempty"` + UpstreamUrl string `protobuf:"bytes,7,opt,name=upstream_url,json=upstreamUrl,proto3" json:"upstream_url,omitempty"` + PackageCount int64 `protobuf:"varint,8,opt,name=package_count,json=packageCount,proto3" json:"package_count,omitempty"` + LogicalBytes int64 `protobuf:"varint,9,opt,name=logical_bytes,json=logicalBytes,proto3" json:"logical_bytes,omitempty"` + PhysicalBytes int64 `protobuf:"varint,10,opt,name=physical_bytes,json=physicalBytes,proto3" json:"physical_bytes,omitempty"` + CurrentRevision string `protobuf:"bytes,11,opt,name=current_revision,json=currentRevision,proto3" json:"current_revision,omitempty"` + PublishStatus string `protobuf:"bytes,12,opt,name=publish_status,json=publishStatus,proto3" json:"publish_status,omitempty"` + LastSyncAt *timestamppb.Timestamp `protobuf:"bytes,13,opt,name=last_sync_at,json=lastSyncAt,proto3" json:"last_sync_at,omitempty"` + LastSyncStatus RepositoryTaskStatus `protobuf:"varint,14,opt,name=last_sync_status,json=lastSyncStatus,proto3,enum=trpc.ocmanager.repository.RepositoryTaskStatus" json:"last_sync_status,omitempty"` + NextSyncAt *timestamppb.Timestamp `protobuf:"bytes,15,opt,name=next_sync_at,json=nextSyncAt,proto3" json:"next_sync_at,omitempty"` + RunningTaskId string `protobuf:"bytes,16,opt,name=running_task_id,json=runningTaskId,proto3" json:"running_task_id,omitempty"` + LastSyncErrorCode string `protobuf:"bytes,17,opt,name=last_sync_error_code,json=lastSyncErrorCode,proto3" json:"last_sync_error_code,omitempty"` + LastSyncErrorMessage string `protobuf:"bytes,18,opt,name=last_sync_error_message,json=lastSyncErrorMessage,proto3" json:"last_sync_error_message,omitempty"` + LastSyncErrorZhMessage string `protobuf:"bytes,19,opt,name=last_sync_error_zh_message,json=lastSyncErrorZhMessage,proto3" json:"last_sync_error_zh_message,omitempty"` + LastSyncErrorZhRecommendedAction string `protobuf:"bytes,20,opt,name=last_sync_error_zh_recommended_action,json=lastSyncErrorZhRecommendedAction,proto3" json:"last_sync_error_zh_recommended_action,omitempty"` +} + +func (x *RepositoryNodeRepositorySummary) Reset() { + *x = RepositoryNodeRepositorySummary{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryNodeRepositorySummary) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryNodeRepositorySummary) ProtoMessage() {} + +func (x *RepositoryNodeRepositorySummary) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[15] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryNodeRepositorySummary.ProtoReflect.Descriptor instead. +func (*RepositoryNodeRepositorySummary) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{15} +} + +func (x *RepositoryNodeRepositorySummary) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *RepositoryNodeRepositorySummary) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *RepositoryNodeRepositorySummary) GetSystemProduct() string { + if x != nil { + return x.SystemProduct + } + return "" +} + +func (x *RepositoryNodeRepositorySummary) GetProductVersion() string { + if x != nil { + return x.ProductVersion + } + return "" +} + +func (x *RepositoryNodeRepositorySummary) GetRepositoryType() RepositoryType { + if x != nil { + return x.RepositoryType + } + return RepositoryType_REPOSITORY_TYPE_UNSPECIFIED +} + +func (x *RepositoryNodeRepositorySummary) GetArchitecture() RepositoryArchitecture { + if x != nil { + return x.Architecture + } + return RepositoryArchitecture_REPOSITORY_ARCHITECTURE_UNSPECIFIED +} + +func (x *RepositoryNodeRepositorySummary) GetUpstreamUrl() string { + if x != nil { + return x.UpstreamUrl + } + return "" +} + +func (x *RepositoryNodeRepositorySummary) GetPackageCount() int64 { + if x != nil { + return x.PackageCount + } + return 0 +} + +func (x *RepositoryNodeRepositorySummary) GetLogicalBytes() int64 { + if x != nil { + return x.LogicalBytes + } + return 0 +} + +func (x *RepositoryNodeRepositorySummary) GetPhysicalBytes() int64 { + if x != nil { + return x.PhysicalBytes + } + return 0 +} + +func (x *RepositoryNodeRepositorySummary) GetCurrentRevision() string { + if x != nil { + return x.CurrentRevision + } + return "" +} + +func (x *RepositoryNodeRepositorySummary) GetPublishStatus() string { + if x != nil { + return x.PublishStatus + } + return "" +} + +func (x *RepositoryNodeRepositorySummary) GetLastSyncAt() *timestamppb.Timestamp { + if x != nil { + return x.LastSyncAt + } + return nil +} + +func (x *RepositoryNodeRepositorySummary) GetLastSyncStatus() RepositoryTaskStatus { + if x != nil { + return x.LastSyncStatus + } + return RepositoryTaskStatus_REPOSITORY_TASK_STATUS_UNSPECIFIED +} + +func (x *RepositoryNodeRepositorySummary) GetNextSyncAt() *timestamppb.Timestamp { + if x != nil { + return x.NextSyncAt + } + return nil +} + +func (x *RepositoryNodeRepositorySummary) GetRunningTaskId() string { + if x != nil { + return x.RunningTaskId + } + return "" +} + +func (x *RepositoryNodeRepositorySummary) GetLastSyncErrorCode() string { + if x != nil { + return x.LastSyncErrorCode + } + return "" +} + +func (x *RepositoryNodeRepositorySummary) GetLastSyncErrorMessage() string { + if x != nil { + return x.LastSyncErrorMessage + } + return "" +} + +func (x *RepositoryNodeRepositorySummary) GetLastSyncErrorZhMessage() string { + if x != nil { + return x.LastSyncErrorZhMessage + } + return "" +} + +func (x *RepositoryNodeRepositorySummary) GetLastSyncErrorZhRecommendedAction() string { + if x != nil { + return x.LastSyncErrorZhRecommendedAction + } + return "" +} + +type CreateRepositoryStorageNodeRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Spec *RepositoryStorageNodeSpec `protobuf:"bytes,1,opt,name=spec,proto3" json:"spec,omitempty"` +} + +func (x *CreateRepositoryStorageNodeRequest) Reset() { + *x = CreateRepositoryStorageNodeRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateRepositoryStorageNodeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateRepositoryStorageNodeRequest) ProtoMessage() {} + +func (x *CreateRepositoryStorageNodeRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[16] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateRepositoryStorageNodeRequest.ProtoReflect.Descriptor instead. +func (*CreateRepositoryStorageNodeRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{16} +} + +func (x *CreateRepositoryStorageNodeRequest) GetSpec() *RepositoryStorageNodeSpec { + if x != nil { + return x.Spec + } + return nil +} + +type UpdateRepositoryStorageNodeRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + StorageNodeId string `protobuf:"bytes,1,opt,name=storage_node_id,json=storageNodeId,proto3" json:"storage_node_id,omitempty"` + Spec *RepositoryStorageNodeSpec `protobuf:"bytes,2,opt,name=spec,proto3" json:"spec,omitempty"` +} + +func (x *UpdateRepositoryStorageNodeRequest) Reset() { + *x = UpdateRepositoryStorageNodeRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UpdateRepositoryStorageNodeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateRepositoryStorageNodeRequest) ProtoMessage() {} + +func (x *UpdateRepositoryStorageNodeRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[17] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateRepositoryStorageNodeRequest.ProtoReflect.Descriptor instead. +func (*UpdateRepositoryStorageNodeRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{17} +} + +func (x *UpdateRepositoryStorageNodeRequest) GetStorageNodeId() string { + if x != nil { + return x.StorageNodeId + } + return "" +} + +func (x *UpdateRepositoryStorageNodeRequest) GetSpec() *RepositoryStorageNodeSpec { + if x != nil { + return x.Spec + } + return nil +} + +type DescribeRepositoryStorageNodeRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + StorageNodeId string `protobuf:"bytes,1,opt,name=storage_node_id,json=storageNodeId,proto3" json:"storage_node_id,omitempty"` +} + +func (x *DescribeRepositoryStorageNodeRequest) Reset() { + *x = DescribeRepositoryStorageNodeRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DescribeRepositoryStorageNodeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeRepositoryStorageNodeRequest) ProtoMessage() {} + +func (x *DescribeRepositoryStorageNodeRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[18] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeRepositoryStorageNodeRequest.ProtoReflect.Descriptor instead. +func (*DescribeRepositoryStorageNodeRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{18} +} + +func (x *DescribeRepositoryStorageNodeRequest) GetStorageNodeId() string { + if x != nil { + return x.StorageNodeId + } + return "" +} + +type ListRepositoryStorageNodesRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ProjectId int64 `protobuf:"varint,1,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + Status RepositoryStorageNodeStatus `protobuf:"varint,2,opt,name=status,proto3,enum=trpc.ocmanager.repository.RepositoryStorageNodeStatus" json:"status,omitempty"` + Keyword string `protobuf:"bytes,3,opt,name=keyword,proto3" json:"keyword,omitempty"` + PageRequest *common.PageRequest `protobuf:"bytes,10,opt,name=page_request,json=pageRequest,proto3" json:"page_request,omitempty"` +} + +func (x *ListRepositoryStorageNodesRequest) Reset() { + *x = ListRepositoryStorageNodesRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListRepositoryStorageNodesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRepositoryStorageNodesRequest) ProtoMessage() {} + +func (x *ListRepositoryStorageNodesRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[19] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRepositoryStorageNodesRequest.ProtoReflect.Descriptor instead. +func (*ListRepositoryStorageNodesRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{19} +} + +func (x *ListRepositoryStorageNodesRequest) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +func (x *ListRepositoryStorageNodesRequest) GetStatus() RepositoryStorageNodeStatus { + if x != nil { + return x.Status + } + return RepositoryStorageNodeStatus_REPOSITORY_STORAGE_NODE_STATUS_UNSPECIFIED +} + +func (x *ListRepositoryStorageNodesRequest) GetKeyword() string { + if x != nil { + return x.Keyword + } + return "" +} + +func (x *ListRepositoryStorageNodesRequest) GetPageRequest() *common.PageRequest { + if x != nil { + return x.PageRequest + } + return nil +} + +type DeleteRepositoryStorageNodeRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + StorageNodeId string `protobuf:"bytes,1,opt,name=storage_node_id,json=storageNodeId,proto3" json:"storage_node_id,omitempty"` + ConfirmDelete bool `protobuf:"varint,2,opt,name=confirm_delete,json=confirmDelete,proto3" json:"confirm_delete,omitempty"` +} + +func (x *DeleteRepositoryStorageNodeRequest) Reset() { + *x = DeleteRepositoryStorageNodeRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteRepositoryStorageNodeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteRepositoryStorageNodeRequest) ProtoMessage() {} + +func (x *DeleteRepositoryStorageNodeRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[20] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteRepositoryStorageNodeRequest.ProtoReflect.Descriptor instead. +func (*DeleteRepositoryStorageNodeRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{20} +} + +func (x *DeleteRepositoryStorageNodeRequest) GetStorageNodeId() string { + if x != nil { + return x.StorageNodeId + } + return "" +} + +func (x *DeleteRepositoryStorageNodeRequest) GetConfirmDelete() bool { + if x != nil { + return x.ConfirmDelete + } + return false +} + +type ProbeRepositoryStorageNodeRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + StorageNodeId string `protobuf:"bytes,1,opt,name=storage_node_id,json=storageNodeId,proto3" json:"storage_node_id,omitempty"` +} + +func (x *ProbeRepositoryStorageNodeRequest) Reset() { + *x = ProbeRepositoryStorageNodeRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ProbeRepositoryStorageNodeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProbeRepositoryStorageNodeRequest) ProtoMessage() {} + +func (x *ProbeRepositoryStorageNodeRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[21] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProbeRepositoryStorageNodeRequest.ProtoReflect.Descriptor instead. +func (*ProbeRepositoryStorageNodeRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{21} +} + +func (x *ProbeRepositoryStorageNodeRequest) GetStorageNodeId() string { + if x != nil { + return x.StorageNodeId + } + return "" +} + +type RepositoryStorageNodeResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + StorageNode *RepositoryStorageNode `protobuf:"bytes,1,opt,name=storage_node,json=storageNode,proto3" json:"storage_node,omitempty"` + Repositories []*RepositoryNodeRepositorySummary `protobuf:"bytes,2,rep,name=repositories,proto3" json:"repositories,omitempty"` + MetadataSigningKeys []*RepositoryMetadataSigningKey `protobuf:"bytes,3,rep,name=metadata_signing_keys,json=metadataSigningKeys,proto3" json:"metadata_signing_keys,omitempty"` +} + +func (x *RepositoryStorageNodeResponse) Reset() { + *x = RepositoryStorageNodeResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryStorageNodeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryStorageNodeResponse) ProtoMessage() {} + +func (x *RepositoryStorageNodeResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[22] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryStorageNodeResponse.ProtoReflect.Descriptor instead. +func (*RepositoryStorageNodeResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{22} +} + +func (x *RepositoryStorageNodeResponse) GetStorageNode() *RepositoryStorageNode { + if x != nil { + return x.StorageNode + } + return nil +} + +func (x *RepositoryStorageNodeResponse) GetRepositories() []*RepositoryNodeRepositorySummary { + if x != nil { + return x.Repositories + } + return nil +} + +func (x *RepositoryStorageNodeResponse) GetMetadataSigningKeys() []*RepositoryMetadataSigningKey { + if x != nil { + return x.MetadataSigningKeys + } + return nil +} + +type RepositoryMetadataSigningKey struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + StorageNodeId string `protobuf:"bytes,2,opt,name=storage_node_id,json=storageNodeId,proto3" json:"storage_node_id,omitempty"` + KeyRef string `protobuf:"bytes,3,opt,name=key_ref,json=keyRef,proto3" json:"key_ref,omitempty"` + Fingerprint string `protobuf:"bytes,4,opt,name=fingerprint,proto3" json:"fingerprint,omitempty"` + PublicKeyName string `protobuf:"bytes,5,opt,name=public_key_name,json=publicKeyName,proto3" json:"public_key_name,omitempty"` + PublicKeyUrl string `protobuf:"bytes,6,opt,name=public_key_url,json=publicKeyUrl,proto3" json:"public_key_url,omitempty"` + Status string `protobuf:"bytes,7,opt,name=status,proto3" json:"status,omitempty"` + KeyCreatedAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=key_created_at,json=keyCreatedAt,proto3" json:"key_created_at,omitempty"` + ObservedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=observed_at,json=observedAt,proto3" json:"observed_at,omitempty"` +} + +func (x *RepositoryMetadataSigningKey) Reset() { + *x = RepositoryMetadataSigningKey{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryMetadataSigningKey) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryMetadataSigningKey) ProtoMessage() {} + +func (x *RepositoryMetadataSigningKey) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[23] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryMetadataSigningKey.ProtoReflect.Descriptor instead. +func (*RepositoryMetadataSigningKey) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{23} +} + +func (x *RepositoryMetadataSigningKey) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *RepositoryMetadataSigningKey) GetStorageNodeId() string { + if x != nil { + return x.StorageNodeId + } + return "" +} + +func (x *RepositoryMetadataSigningKey) GetKeyRef() string { + if x != nil { + return x.KeyRef + } + return "" +} + +func (x *RepositoryMetadataSigningKey) GetFingerprint() string { + if x != nil { + return x.Fingerprint + } + return "" +} + +func (x *RepositoryMetadataSigningKey) GetPublicKeyName() string { + if x != nil { + return x.PublicKeyName + } + return "" +} + +func (x *RepositoryMetadataSigningKey) GetPublicKeyUrl() string { + if x != nil { + return x.PublicKeyUrl + } + return "" +} + +func (x *RepositoryMetadataSigningKey) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *RepositoryMetadataSigningKey) GetKeyCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.KeyCreatedAt + } + return nil +} + +func (x *RepositoryMetadataSigningKey) GetObservedAt() *timestamppb.Timestamp { + if x != nil { + return x.ObservedAt + } + return nil +} + +type ListRepositoryStorageNodesResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + StorageNodes []*RepositoryStorageNode `protobuf:"bytes,1,rep,name=storage_nodes,json=storageNodes,proto3" json:"storage_nodes,omitempty"` + TotalNodes int64 `protobuf:"varint,2,opt,name=total_nodes,json=totalNodes,proto3" json:"total_nodes,omitempty"` + OnlineNodes int64 `protobuf:"varint,3,opt,name=online_nodes,json=onlineNodes,proto3" json:"online_nodes,omitempty"` + TotalCapacityBytes int64 `protobuf:"varint,4,opt,name=total_capacity_bytes,json=totalCapacityBytes,proto3" json:"total_capacity_bytes,omitempty"` + UsedCapacityBytes int64 `protobuf:"varint,5,opt,name=used_capacity_bytes,json=usedCapacityBytes,proto3" json:"used_capacity_bytes,omitempty"` + CriticalNodes int64 `protobuf:"varint,6,opt,name=critical_nodes,json=criticalNodes,proto3" json:"critical_nodes,omitempty"` + PageResponse *common.PageResponse `protobuf:"bytes,10,opt,name=page_response,json=pageResponse,proto3" json:"page_response,omitempty"` +} + +func (x *ListRepositoryStorageNodesResponse) Reset() { + *x = ListRepositoryStorageNodesResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListRepositoryStorageNodesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRepositoryStorageNodesResponse) ProtoMessage() {} + +func (x *ListRepositoryStorageNodesResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[24] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRepositoryStorageNodesResponse.ProtoReflect.Descriptor instead. +func (*ListRepositoryStorageNodesResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{24} +} + +func (x *ListRepositoryStorageNodesResponse) GetStorageNodes() []*RepositoryStorageNode { + if x != nil { + return x.StorageNodes + } + return nil +} + +func (x *ListRepositoryStorageNodesResponse) GetTotalNodes() int64 { + if x != nil { + return x.TotalNodes + } + return 0 +} + +func (x *ListRepositoryStorageNodesResponse) GetOnlineNodes() int64 { + if x != nil { + return x.OnlineNodes + } + return 0 +} + +func (x *ListRepositoryStorageNodesResponse) GetTotalCapacityBytes() int64 { + if x != nil { + return x.TotalCapacityBytes + } + return 0 +} + +func (x *ListRepositoryStorageNodesResponse) GetUsedCapacityBytes() int64 { + if x != nil { + return x.UsedCapacityBytes + } + return 0 +} + +func (x *ListRepositoryStorageNodesResponse) GetCriticalNodes() int64 { + if x != nil { + return x.CriticalNodes + } + return 0 +} + +func (x *ListRepositoryStorageNodesResponse) GetPageResponse() *common.PageResponse { + if x != nil { + return x.PageResponse + } + return nil +} + +type RepositoryNodeEnrollmentSpec struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ProjectId int64 `protobuf:"varint,1,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + ControlBaseUrl string `protobuf:"bytes,4,opt,name=control_base_url,json=controlBaseUrl,proto3" json:"control_base_url,omitempty"` + PublishBaseUrl string `protobuf:"bytes,5,opt,name=publish_base_url,json=publishBaseUrl,proto3" json:"publish_base_url,omitempty"` + TlsServerName string `protobuf:"bytes,6,opt,name=tls_server_name,json=tlsServerName,proto3" json:"tls_server_name,omitempty"` + Labels map[string]string `protobuf:"bytes,7,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + ExpectedArchitecture RepositoryArchitecture `protobuf:"varint,8,opt,name=expected_architecture,json=expectedArchitecture,proto3,enum=trpc.ocmanager.repository.RepositoryArchitecture" json:"expected_architecture,omitempty"` + TmsAgentId uint64 `protobuf:"varint,9,opt,name=tms_agent_id,json=tmsAgentId,proto3" json:"tms_agent_id,omitempty"` + ArtifactId string `protobuf:"bytes,10,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"` +} + +func (x *RepositoryNodeEnrollmentSpec) Reset() { + *x = RepositoryNodeEnrollmentSpec{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryNodeEnrollmentSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryNodeEnrollmentSpec) ProtoMessage() {} + +func (x *RepositoryNodeEnrollmentSpec) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[25] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryNodeEnrollmentSpec.ProtoReflect.Descriptor instead. +func (*RepositoryNodeEnrollmentSpec) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{25} +} + +func (x *RepositoryNodeEnrollmentSpec) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +func (x *RepositoryNodeEnrollmentSpec) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *RepositoryNodeEnrollmentSpec) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *RepositoryNodeEnrollmentSpec) GetControlBaseUrl() string { + if x != nil { + return x.ControlBaseUrl + } + return "" +} + +func (x *RepositoryNodeEnrollmentSpec) GetPublishBaseUrl() string { + if x != nil { + return x.PublishBaseUrl + } + return "" +} + +func (x *RepositoryNodeEnrollmentSpec) GetTlsServerName() string { + if x != nil { + return x.TlsServerName + } + return "" +} + +func (x *RepositoryNodeEnrollmentSpec) GetLabels() map[string]string { + if x != nil { + return x.Labels + } + return nil +} + +func (x *RepositoryNodeEnrollmentSpec) GetExpectedArchitecture() RepositoryArchitecture { + if x != nil { + return x.ExpectedArchitecture + } + return RepositoryArchitecture_REPOSITORY_ARCHITECTURE_UNSPECIFIED +} + +func (x *RepositoryNodeEnrollmentSpec) GetTmsAgentId() uint64 { + if x != nil { + return x.TmsAgentId + } + return 0 +} + +func (x *RepositoryNodeEnrollmentSpec) GetArtifactId() string { + if x != nil { + return x.ArtifactId + } + return "" +} + +type RepositoryNodeEnrollment struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + StorageNodeId string `protobuf:"bytes,2,opt,name=storage_node_id,json=storageNodeId,proto3" json:"storage_node_id,omitempty"` + ProjectId int64 `protobuf:"varint,3,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + TmsAgentId uint64 `protobuf:"varint,4,opt,name=tms_agent_id,json=tmsAgentId,proto3" json:"tms_agent_id,omitempty"` + Generation int32 `protobuf:"varint,5,opt,name=generation,proto3" json:"generation,omitempty"` + Status RepositoryNodeEnrollmentStatus `protobuf:"varint,6,opt,name=status,proto3,enum=trpc.ocmanager.repository.RepositoryNodeEnrollmentStatus" json:"status,omitempty"` + InstallTaskId string `protobuf:"bytes,7,opt,name=install_task_id,json=installTaskId,proto3" json:"install_task_id,omitempty"` + ExpectedArchitecture RepositoryArchitecture `protobuf:"varint,8,opt,name=expected_architecture,json=expectedArchitecture,proto3,enum=trpc.ocmanager.repository.RepositoryArchitecture" json:"expected_architecture,omitempty"` + ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` + LastErrorCode string `protobuf:"bytes,10,opt,name=last_error_code,json=lastErrorCode,proto3" json:"last_error_code,omitempty"` + LastErrorMessage string `protobuf:"bytes,11,opt,name=last_error_message,json=lastErrorMessage,proto3" json:"last_error_message,omitempty"` + CreatedBy string `protobuf:"bytes,12,opt,name=created_by,json=createdBy,proto3" json:"created_by,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,13,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,14,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` + TmsTaskId string `protobuf:"bytes,15,opt,name=tms_task_id,json=tmsTaskId,proto3" json:"tms_task_id,omitempty"` + AllowlistEntryId string `protobuf:"bytes,16,opt,name=allowlist_entry_id,json=allowlistEntryId,proto3" json:"allowlist_entry_id,omitempty"` + ArtifactId string `protobuf:"bytes,17,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"` + AuthorizationId string `protobuf:"bytes,18,opt,name=authorization_id,json=authorizationId,proto3" json:"authorization_id,omitempty"` + CsrSha256 string `protobuf:"bytes,19,opt,name=csr_sha256,json=csrSha256,proto3" json:"csr_sha256,omitempty"` + IssuanceStatus string `protobuf:"bytes,20,opt,name=issuance_status,json=issuanceStatus,proto3" json:"issuance_status,omitempty"` +} + +func (x *RepositoryNodeEnrollment) Reset() { + *x = RepositoryNodeEnrollment{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryNodeEnrollment) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryNodeEnrollment) ProtoMessage() {} + +func (x *RepositoryNodeEnrollment) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[26] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryNodeEnrollment.ProtoReflect.Descriptor instead. +func (*RepositoryNodeEnrollment) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{26} +} + +func (x *RepositoryNodeEnrollment) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *RepositoryNodeEnrollment) GetStorageNodeId() string { + if x != nil { + return x.StorageNodeId + } + return "" +} + +func (x *RepositoryNodeEnrollment) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +func (x *RepositoryNodeEnrollment) GetTmsAgentId() uint64 { + if x != nil { + return x.TmsAgentId + } + return 0 +} + +func (x *RepositoryNodeEnrollment) GetGeneration() int32 { + if x != nil { + return x.Generation + } + return 0 +} + +func (x *RepositoryNodeEnrollment) GetStatus() RepositoryNodeEnrollmentStatus { + if x != nil { + return x.Status + } + return RepositoryNodeEnrollmentStatus_REPOSITORY_NODE_ENROLLMENT_STATUS_UNSPECIFIED +} + +func (x *RepositoryNodeEnrollment) GetInstallTaskId() string { + if x != nil { + return x.InstallTaskId + } + return "" +} + +func (x *RepositoryNodeEnrollment) GetExpectedArchitecture() RepositoryArchitecture { + if x != nil { + return x.ExpectedArchitecture + } + return RepositoryArchitecture_REPOSITORY_ARCHITECTURE_UNSPECIFIED +} + +func (x *RepositoryNodeEnrollment) GetExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.ExpiresAt + } + return nil +} + +func (x *RepositoryNodeEnrollment) GetLastErrorCode() string { + if x != nil { + return x.LastErrorCode + } + return "" +} + +func (x *RepositoryNodeEnrollment) GetLastErrorMessage() string { + if x != nil { + return x.LastErrorMessage + } + return "" +} + +func (x *RepositoryNodeEnrollment) GetCreatedBy() string { + if x != nil { + return x.CreatedBy + } + return "" +} + +func (x *RepositoryNodeEnrollment) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *RepositoryNodeEnrollment) GetUpdatedAt() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedAt + } + return nil +} + +func (x *RepositoryNodeEnrollment) GetTmsTaskId() string { + if x != nil { + return x.TmsTaskId + } + return "" +} + +func (x *RepositoryNodeEnrollment) GetAllowlistEntryId() string { + if x != nil { + return x.AllowlistEntryId + } + return "" +} + +func (x *RepositoryNodeEnrollment) GetArtifactId() string { + if x != nil { + return x.ArtifactId + } + return "" +} + +func (x *RepositoryNodeEnrollment) GetAuthorizationId() string { + if x != nil { + return x.AuthorizationId + } + return "" +} + +func (x *RepositoryNodeEnrollment) GetCsrSha256() string { + if x != nil { + return x.CsrSha256 + } + return "" +} + +func (x *RepositoryNodeEnrollment) GetIssuanceStatus() string { + if x != nil { + return x.IssuanceStatus + } + return "" +} + +type CreateRepositoryNodeEnrollmentRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Spec *RepositoryNodeEnrollmentSpec `protobuf:"bytes,1,opt,name=spec,proto3" json:"spec,omitempty"` +} + +func (x *CreateRepositoryNodeEnrollmentRequest) Reset() { + *x = CreateRepositoryNodeEnrollmentRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateRepositoryNodeEnrollmentRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateRepositoryNodeEnrollmentRequest) ProtoMessage() {} + +func (x *CreateRepositoryNodeEnrollmentRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[27] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateRepositoryNodeEnrollmentRequest.ProtoReflect.Descriptor instead. +func (*CreateRepositoryNodeEnrollmentRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{27} +} + +func (x *CreateRepositoryNodeEnrollmentRequest) GetSpec() *RepositoryNodeEnrollmentSpec { + if x != nil { + return x.Spec + } + return nil +} + +type RepositoryNodeEnrollmentResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + StorageNode *RepositoryStorageNode `protobuf:"bytes,1,opt,name=storage_node,json=storageNode,proto3" json:"storage_node,omitempty"` + Enrollment *RepositoryNodeEnrollment `protobuf:"bytes,2,opt,name=enrollment,proto3" json:"enrollment,omitempty"` + Task *RepositoryTask `protobuf:"bytes,3,opt,name=task,proto3" json:"task,omitempty"` + Target *RepositoryTaskTarget `protobuf:"bytes,4,opt,name=target,proto3" json:"target,omitempty"` + BootstrapToken string `protobuf:"bytes,5,opt,name=bootstrap_token,json=bootstrapToken,proto3" json:"bootstrap_token,omitempty"` + BootstrapExpiresAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=bootstrap_expires_at,json=bootstrapExpiresAt,proto3" json:"bootstrap_expires_at,omitempty"` +} + +func (x *RepositoryNodeEnrollmentResponse) Reset() { + *x = RepositoryNodeEnrollmentResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryNodeEnrollmentResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryNodeEnrollmentResponse) ProtoMessage() {} + +func (x *RepositoryNodeEnrollmentResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[28] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryNodeEnrollmentResponse.ProtoReflect.Descriptor instead. +func (*RepositoryNodeEnrollmentResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{28} +} + +func (x *RepositoryNodeEnrollmentResponse) GetStorageNode() *RepositoryStorageNode { + if x != nil { + return x.StorageNode + } + return nil +} + +func (x *RepositoryNodeEnrollmentResponse) GetEnrollment() *RepositoryNodeEnrollment { + if x != nil { + return x.Enrollment + } + return nil +} + +func (x *RepositoryNodeEnrollmentResponse) GetTask() *RepositoryTask { + if x != nil { + return x.Task + } + return nil +} + +func (x *RepositoryNodeEnrollmentResponse) GetTarget() *RepositoryTaskTarget { + if x != nil { + return x.Target + } + return nil +} + +func (x *RepositoryNodeEnrollmentResponse) GetBootstrapToken() string { + if x != nil { + return x.BootstrapToken + } + return "" +} + +func (x *RepositoryNodeEnrollmentResponse) GetBootstrapExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.BootstrapExpiresAt + } + return nil +} + +type RepositoryNodeArtifactSpec struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` + Release string `protobuf:"bytes,3,opt,name=release,proto3" json:"release,omitempty"` + Architecture RepositoryArchitecture `protobuf:"varint,4,opt,name=architecture,proto3,enum=trpc.ocmanager.repository.RepositoryArchitecture" json:"architecture,omitempty"` + FileName string `protobuf:"bytes,5,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"` + SizeBytes int64 `protobuf:"varint,6,opt,name=size_bytes,json=sizeBytes,proto3" json:"size_bytes,omitempty"` + Sha256 string `protobuf:"bytes,7,opt,name=sha256,proto3" json:"sha256,omitempty"` + BuildCommit string `protobuf:"bytes,8,opt,name=build_commit,json=buildCommit,proto3" json:"build_commit,omitempty"` + RelativePath string `protobuf:"bytes,9,opt,name=relative_path,json=relativePath,proto3" json:"relative_path,omitempty"` + ProjectId int64 `protobuf:"varint,10,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + SourceRepositoryId string `protobuf:"bytes,11,opt,name=source_repository_id,json=sourceRepositoryId,proto3" json:"source_repository_id,omitempty"` + RevisionId string `protobuf:"bytes,12,opt,name=revision_id,json=revisionId,proto3" json:"revision_id,omitempty"` + PackageId string `protobuf:"bytes,13,opt,name=package_id,json=packageId,proto3" json:"package_id,omitempty"` +} + +func (x *RepositoryNodeArtifactSpec) Reset() { + *x = RepositoryNodeArtifactSpec{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryNodeArtifactSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryNodeArtifactSpec) ProtoMessage() {} + +func (x *RepositoryNodeArtifactSpec) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[29] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryNodeArtifactSpec.ProtoReflect.Descriptor instead. +func (*RepositoryNodeArtifactSpec) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{29} +} + +func (x *RepositoryNodeArtifactSpec) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *RepositoryNodeArtifactSpec) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *RepositoryNodeArtifactSpec) GetRelease() string { + if x != nil { + return x.Release + } + return "" +} + +func (x *RepositoryNodeArtifactSpec) GetArchitecture() RepositoryArchitecture { + if x != nil { + return x.Architecture + } + return RepositoryArchitecture_REPOSITORY_ARCHITECTURE_UNSPECIFIED +} + +func (x *RepositoryNodeArtifactSpec) GetFileName() string { + if x != nil { + return x.FileName + } + return "" +} + +func (x *RepositoryNodeArtifactSpec) GetSizeBytes() int64 { + if x != nil { + return x.SizeBytes + } + return 0 +} + +func (x *RepositoryNodeArtifactSpec) GetSha256() string { + if x != nil { + return x.Sha256 + } + return "" +} + +func (x *RepositoryNodeArtifactSpec) GetBuildCommit() string { + if x != nil { + return x.BuildCommit + } + return "" +} + +func (x *RepositoryNodeArtifactSpec) GetRelativePath() string { + if x != nil { + return x.RelativePath + } + return "" +} + +func (x *RepositoryNodeArtifactSpec) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +func (x *RepositoryNodeArtifactSpec) GetSourceRepositoryId() string { + if x != nil { + return x.SourceRepositoryId + } + return "" +} + +func (x *RepositoryNodeArtifactSpec) GetRevisionId() string { + if x != nil { + return x.RevisionId + } + return "" +} + +func (x *RepositoryNodeArtifactSpec) GetPackageId() string { + if x != nil { + return x.PackageId + } + return "" +} + +type RepositoryNodeArtifact struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Version string `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` + Release string `protobuf:"bytes,4,opt,name=release,proto3" json:"release,omitempty"` + Architecture RepositoryArchitecture `protobuf:"varint,5,opt,name=architecture,proto3,enum=trpc.ocmanager.repository.RepositoryArchitecture" json:"architecture,omitempty"` + FileName string `protobuf:"bytes,6,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"` + SizeBytes int64 `protobuf:"varint,7,opt,name=size_bytes,json=sizeBytes,proto3" json:"size_bytes,omitempty"` + Sha256 string `protobuf:"bytes,8,opt,name=sha256,proto3" json:"sha256,omitempty"` + BuildCommit string `protobuf:"bytes,9,opt,name=build_commit,json=buildCommit,proto3" json:"build_commit,omitempty"` + RelativePath string `protobuf:"bytes,10,opt,name=relative_path,json=relativePath,proto3" json:"relative_path,omitempty"` + Status string `protobuf:"bytes,11,opt,name=status,proto3" json:"status,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + ProjectId int64 `protobuf:"varint,13,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + SourceKind string `protobuf:"bytes,14,opt,name=source_kind,json=sourceKind,proto3" json:"source_kind,omitempty"` + SourceRepositoryId string `protobuf:"bytes,15,opt,name=source_repository_id,json=sourceRepositoryId,proto3" json:"source_repository_id,omitempty"` + RevisionId string `protobuf:"bytes,16,opt,name=revision_id,json=revisionId,proto3" json:"revision_id,omitempty"` + PackageId string `protobuf:"bytes,17,opt,name=package_id,json=packageId,proto3" json:"package_id,omitempty"` + Epoch uint64 `protobuf:"varint,18,opt,name=epoch,proto3" json:"epoch,omitempty"` + Nevra string `protobuf:"bytes,19,opt,name=nevra,proto3" json:"nevra,omitempty"` + ImmutableUrl string `protobuf:"bytes,20,opt,name=immutable_url,json=immutableUrl,proto3" json:"immutable_url,omitempty"` +} + +func (x *RepositoryNodeArtifact) Reset() { + *x = RepositoryNodeArtifact{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryNodeArtifact) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryNodeArtifact) ProtoMessage() {} + +func (x *RepositoryNodeArtifact) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[30] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryNodeArtifact.ProtoReflect.Descriptor instead. +func (*RepositoryNodeArtifact) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{30} +} + +func (x *RepositoryNodeArtifact) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *RepositoryNodeArtifact) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *RepositoryNodeArtifact) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *RepositoryNodeArtifact) GetRelease() string { + if x != nil { + return x.Release + } + return "" +} + +func (x *RepositoryNodeArtifact) GetArchitecture() RepositoryArchitecture { + if x != nil { + return x.Architecture + } + return RepositoryArchitecture_REPOSITORY_ARCHITECTURE_UNSPECIFIED +} + +func (x *RepositoryNodeArtifact) GetFileName() string { + if x != nil { + return x.FileName + } + return "" +} + +func (x *RepositoryNodeArtifact) GetSizeBytes() int64 { + if x != nil { + return x.SizeBytes + } + return 0 +} + +func (x *RepositoryNodeArtifact) GetSha256() string { + if x != nil { + return x.Sha256 + } + return "" +} + +func (x *RepositoryNodeArtifact) GetBuildCommit() string { + if x != nil { + return x.BuildCommit + } + return "" +} + +func (x *RepositoryNodeArtifact) GetRelativePath() string { + if x != nil { + return x.RelativePath + } + return "" +} + +func (x *RepositoryNodeArtifact) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *RepositoryNodeArtifact) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *RepositoryNodeArtifact) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +func (x *RepositoryNodeArtifact) GetSourceKind() string { + if x != nil { + return x.SourceKind + } + return "" +} + +func (x *RepositoryNodeArtifact) GetSourceRepositoryId() string { + if x != nil { + return x.SourceRepositoryId + } + return "" +} + +func (x *RepositoryNodeArtifact) GetRevisionId() string { + if x != nil { + return x.RevisionId + } + return "" +} + +func (x *RepositoryNodeArtifact) GetPackageId() string { + if x != nil { + return x.PackageId + } + return "" +} + +func (x *RepositoryNodeArtifact) GetEpoch() uint64 { + if x != nil { + return x.Epoch + } + return 0 +} + +func (x *RepositoryNodeArtifact) GetNevra() string { + if x != nil { + return x.Nevra + } + return "" +} + +func (x *RepositoryNodeArtifact) GetImmutableUrl() string { + if x != nil { + return x.ImmutableUrl + } + return "" +} + +type RegisterRepositoryNodeArtifactRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Spec *RepositoryNodeArtifactSpec `protobuf:"bytes,1,opt,name=spec,proto3" json:"spec,omitempty"` +} + +func (x *RegisterRepositoryNodeArtifactRequest) Reset() { + *x = RegisterRepositoryNodeArtifactRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RegisterRepositoryNodeArtifactRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterRepositoryNodeArtifactRequest) ProtoMessage() {} + +func (x *RegisterRepositoryNodeArtifactRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[31] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RegisterRepositoryNodeArtifactRequest.ProtoReflect.Descriptor instead. +func (*RegisterRepositoryNodeArtifactRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{31} +} + +func (x *RegisterRepositoryNodeArtifactRequest) GetSpec() *RepositoryNodeArtifactSpec { + if x != nil { + return x.Spec + } + return nil +} + +type RepositoryNodeArtifactResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Artifact *RepositoryNodeArtifact `protobuf:"bytes,1,opt,name=artifact,proto3" json:"artifact,omitempty"` +} + +func (x *RepositoryNodeArtifactResponse) Reset() { + *x = RepositoryNodeArtifactResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryNodeArtifactResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryNodeArtifactResponse) ProtoMessage() {} + +func (x *RepositoryNodeArtifactResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[32] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryNodeArtifactResponse.ProtoReflect.Descriptor instead. +func (*RepositoryNodeArtifactResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{32} +} + +func (x *RepositoryNodeArtifactResponse) GetArtifact() *RepositoryNodeArtifact { + if x != nil { + return x.Artifact + } + return nil +} + +type ListRepositoryNodeArtifactsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` + ProjectId int64 `protobuf:"varint,2,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` +} + +func (x *ListRepositoryNodeArtifactsRequest) Reset() { + *x = ListRepositoryNodeArtifactsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListRepositoryNodeArtifactsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRepositoryNodeArtifactsRequest) ProtoMessage() {} + +func (x *ListRepositoryNodeArtifactsRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[33] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRepositoryNodeArtifactsRequest.ProtoReflect.Descriptor instead. +func (*ListRepositoryNodeArtifactsRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{33} +} + +func (x *ListRepositoryNodeArtifactsRequest) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *ListRepositoryNodeArtifactsRequest) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +type ListRepositoryNodeArtifactsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Artifacts []*RepositoryNodeArtifact `protobuf:"bytes,1,rep,name=artifacts,proto3" json:"artifacts,omitempty"` +} + +func (x *ListRepositoryNodeArtifactsResponse) Reset() { + *x = ListRepositoryNodeArtifactsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListRepositoryNodeArtifactsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRepositoryNodeArtifactsResponse) ProtoMessage() {} + +func (x *ListRepositoryNodeArtifactsResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[34] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRepositoryNodeArtifactsResponse.ProtoReflect.Descriptor instead. +func (*ListRepositoryNodeArtifactsResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{34} +} + +func (x *ListRepositoryNodeArtifactsResponse) GetArtifacts() []*RepositoryNodeArtifact { + if x != nil { + return x.Artifacts + } + return nil +} + +type DownloadRepositoryNodeArtifactRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ArtifactId string `protobuf:"bytes,1,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"` +} + +func (x *DownloadRepositoryNodeArtifactRequest) Reset() { + *x = DownloadRepositoryNodeArtifactRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DownloadRepositoryNodeArtifactRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DownloadRepositoryNodeArtifactRequest) ProtoMessage() {} + +func (x *DownloadRepositoryNodeArtifactRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[35] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DownloadRepositoryNodeArtifactRequest.ProtoReflect.Descriptor instead. +func (*DownloadRepositoryNodeArtifactRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{35} +} + +func (x *DownloadRepositoryNodeArtifactRequest) GetArtifactId() string { + if x != nil { + return x.ArtifactId + } + return "" +} + +type RepositoryNodeInstallSourceSpec struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ProjectId int64 `protobuf:"varint,1,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + RepositoryId string `protobuf:"bytes,2,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + Architecture RepositoryArchitecture `protobuf:"varint,3,opt,name=architecture,proto3,enum=trpc.ocmanager.repository.RepositoryArchitecture" json:"architecture,omitempty"` + Status string `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"` +} + +func (x *RepositoryNodeInstallSourceSpec) Reset() { + *x = RepositoryNodeInstallSourceSpec{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryNodeInstallSourceSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryNodeInstallSourceSpec) ProtoMessage() {} + +func (x *RepositoryNodeInstallSourceSpec) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[36] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryNodeInstallSourceSpec.ProtoReflect.Descriptor instead. +func (*RepositoryNodeInstallSourceSpec) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{36} +} + +func (x *RepositoryNodeInstallSourceSpec) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +func (x *RepositoryNodeInstallSourceSpec) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *RepositoryNodeInstallSourceSpec) GetArchitecture() RepositoryArchitecture { + if x != nil { + return x.Architecture + } + return RepositoryArchitecture_REPOSITORY_ARCHITECTURE_UNSPECIFIED +} + +func (x *RepositoryNodeInstallSourceSpec) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +type RepositoryNodeInstallSource struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + ProjectId int64 `protobuf:"varint,2,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + RepositoryId string `protobuf:"bytes,3,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + Architecture RepositoryArchitecture `protobuf:"varint,4,opt,name=architecture,proto3,enum=trpc.ocmanager.repository.RepositoryArchitecture" json:"architecture,omitempty"` + Status string `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"` + CreatedBy string `protobuf:"bytes,6,opt,name=created_by,json=createdBy,proto3" json:"created_by,omitempty"` + UpdatedBy string `protobuf:"bytes,7,opt,name=updated_by,json=updatedBy,proto3" json:"updated_by,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` +} + +func (x *RepositoryNodeInstallSource) Reset() { + *x = RepositoryNodeInstallSource{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryNodeInstallSource) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryNodeInstallSource) ProtoMessage() {} + +func (x *RepositoryNodeInstallSource) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[37] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryNodeInstallSource.ProtoReflect.Descriptor instead. +func (*RepositoryNodeInstallSource) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{37} +} + +func (x *RepositoryNodeInstallSource) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *RepositoryNodeInstallSource) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +func (x *RepositoryNodeInstallSource) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *RepositoryNodeInstallSource) GetArchitecture() RepositoryArchitecture { + if x != nil { + return x.Architecture + } + return RepositoryArchitecture_REPOSITORY_ARCHITECTURE_UNSPECIFIED +} + +func (x *RepositoryNodeInstallSource) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *RepositoryNodeInstallSource) GetCreatedBy() string { + if x != nil { + return x.CreatedBy + } + return "" +} + +func (x *RepositoryNodeInstallSource) GetUpdatedBy() string { + if x != nil { + return x.UpdatedBy + } + return "" +} + +func (x *RepositoryNodeInstallSource) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *RepositoryNodeInstallSource) GetUpdatedAt() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedAt + } + return nil +} + +type ConfigureRepositoryNodeInstallSourceRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Spec *RepositoryNodeInstallSourceSpec `protobuf:"bytes,1,opt,name=spec,proto3" json:"spec,omitempty"` +} + +func (x *ConfigureRepositoryNodeInstallSourceRequest) Reset() { + *x = ConfigureRepositoryNodeInstallSourceRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ConfigureRepositoryNodeInstallSourceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConfigureRepositoryNodeInstallSourceRequest) ProtoMessage() {} + +func (x *ConfigureRepositoryNodeInstallSourceRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[38] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConfigureRepositoryNodeInstallSourceRequest.ProtoReflect.Descriptor instead. +func (*ConfigureRepositoryNodeInstallSourceRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{38} +} + +func (x *ConfigureRepositoryNodeInstallSourceRequest) GetSpec() *RepositoryNodeInstallSourceSpec { + if x != nil { + return x.Spec + } + return nil +} + +type RepositoryNodeInstallSourceResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + InstallSource *RepositoryNodeInstallSource `protobuf:"bytes,1,opt,name=install_source,json=installSource,proto3" json:"install_source,omitempty"` +} + +func (x *RepositoryNodeInstallSourceResponse) Reset() { + *x = RepositoryNodeInstallSourceResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryNodeInstallSourceResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryNodeInstallSourceResponse) ProtoMessage() {} + +func (x *RepositoryNodeInstallSourceResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[39] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryNodeInstallSourceResponse.ProtoReflect.Descriptor instead. +func (*RepositoryNodeInstallSourceResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{39} +} + +func (x *RepositoryNodeInstallSourceResponse) GetInstallSource() *RepositoryNodeInstallSource { + if x != nil { + return x.InstallSource + } + return nil +} + +type ListRepositoryNodeInstallSourcesRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ProjectId int64 `protobuf:"varint,1,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` +} + +func (x *ListRepositoryNodeInstallSourcesRequest) Reset() { + *x = ListRepositoryNodeInstallSourcesRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListRepositoryNodeInstallSourcesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRepositoryNodeInstallSourcesRequest) ProtoMessage() {} + +func (x *ListRepositoryNodeInstallSourcesRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[40] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRepositoryNodeInstallSourcesRequest.ProtoReflect.Descriptor instead. +func (*ListRepositoryNodeInstallSourcesRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{40} +} + +func (x *ListRepositoryNodeInstallSourcesRequest) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +type ListRepositoryNodeInstallSourcesResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + InstallSources []*RepositoryNodeInstallSource `protobuf:"bytes,1,rep,name=install_sources,json=installSources,proto3" json:"install_sources,omitempty"` +} + +func (x *ListRepositoryNodeInstallSourcesResponse) Reset() { + *x = ListRepositoryNodeInstallSourcesResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListRepositoryNodeInstallSourcesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRepositoryNodeInstallSourcesResponse) ProtoMessage() {} + +func (x *ListRepositoryNodeInstallSourcesResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[41] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRepositoryNodeInstallSourcesResponse.ProtoReflect.Descriptor instead. +func (*ListRepositoryNodeInstallSourcesResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{41} +} + +func (x *ListRepositoryNodeInstallSourcesResponse) GetInstallSources() []*RepositoryNodeInstallSource { + if x != nil { + return x.InstallSources + } + return nil +} + +type RepositoryNodeRegistrationAllowlistSpec struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ProjectId int64 `protobuf:"varint,1,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + TmsAgentId uint64 `protobuf:"varint,2,opt,name=tms_agent_id,json=tmsAgentId,proto3" json:"tms_agent_id,omitempty"` + ExpectedHostname string `protobuf:"bytes,3,opt,name=expected_hostname,json=expectedHostname,proto3" json:"expected_hostname,omitempty"` + ExpectedIp string `protobuf:"bytes,4,opt,name=expected_ip,json=expectedIp,proto3" json:"expected_ip,omitempty"` + ExpectedArchitecture RepositoryArchitecture `protobuf:"varint,5,opt,name=expected_architecture,json=expectedArchitecture,proto3,enum=trpc.ocmanager.repository.RepositoryArchitecture" json:"expected_architecture,omitempty"` + Status string `protobuf:"bytes,6,opt,name=status,proto3" json:"status,omitempty"` + ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` +} + +func (x *RepositoryNodeRegistrationAllowlistSpec) Reset() { + *x = RepositoryNodeRegistrationAllowlistSpec{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryNodeRegistrationAllowlistSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryNodeRegistrationAllowlistSpec) ProtoMessage() {} + +func (x *RepositoryNodeRegistrationAllowlistSpec) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[42] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryNodeRegistrationAllowlistSpec.ProtoReflect.Descriptor instead. +func (*RepositoryNodeRegistrationAllowlistSpec) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{42} +} + +func (x *RepositoryNodeRegistrationAllowlistSpec) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +func (x *RepositoryNodeRegistrationAllowlistSpec) GetTmsAgentId() uint64 { + if x != nil { + return x.TmsAgentId + } + return 0 +} + +func (x *RepositoryNodeRegistrationAllowlistSpec) GetExpectedHostname() string { + if x != nil { + return x.ExpectedHostname + } + return "" +} + +func (x *RepositoryNodeRegistrationAllowlistSpec) GetExpectedIp() string { + if x != nil { + return x.ExpectedIp + } + return "" +} + +func (x *RepositoryNodeRegistrationAllowlistSpec) GetExpectedArchitecture() RepositoryArchitecture { + if x != nil { + return x.ExpectedArchitecture + } + return RepositoryArchitecture_REPOSITORY_ARCHITECTURE_UNSPECIFIED +} + +func (x *RepositoryNodeRegistrationAllowlistSpec) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *RepositoryNodeRegistrationAllowlistSpec) GetExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.ExpiresAt + } + return nil +} + +type RepositoryNodeRegistrationAllowlist struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + ProjectId int64 `protobuf:"varint,2,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + TmsAgentId uint64 `protobuf:"varint,3,opt,name=tms_agent_id,json=tmsAgentId,proto3" json:"tms_agent_id,omitempty"` + ExpectedHostname string `protobuf:"bytes,4,opt,name=expected_hostname,json=expectedHostname,proto3" json:"expected_hostname,omitempty"` + ExpectedIp string `protobuf:"bytes,5,opt,name=expected_ip,json=expectedIp,proto3" json:"expected_ip,omitempty"` + ExpectedArchitecture RepositoryArchitecture `protobuf:"varint,6,opt,name=expected_architecture,json=expectedArchitecture,proto3,enum=trpc.ocmanager.repository.RepositoryArchitecture" json:"expected_architecture,omitempty"` + Status string `protobuf:"bytes,7,opt,name=status,proto3" json:"status,omitempty"` + ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` + CreatedBy string `protobuf:"bytes,9,opt,name=created_by,json=createdBy,proto3" json:"created_by,omitempty"` + UpdatedBy string `protobuf:"bytes,10,opt,name=updated_by,json=updatedBy,proto3" json:"updated_by,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` +} + +func (x *RepositoryNodeRegistrationAllowlist) Reset() { + *x = RepositoryNodeRegistrationAllowlist{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[43] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryNodeRegistrationAllowlist) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryNodeRegistrationAllowlist) ProtoMessage() {} + +func (x *RepositoryNodeRegistrationAllowlist) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[43] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryNodeRegistrationAllowlist.ProtoReflect.Descriptor instead. +func (*RepositoryNodeRegistrationAllowlist) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{43} +} + +func (x *RepositoryNodeRegistrationAllowlist) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *RepositoryNodeRegistrationAllowlist) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +func (x *RepositoryNodeRegistrationAllowlist) GetTmsAgentId() uint64 { + if x != nil { + return x.TmsAgentId + } + return 0 +} + +func (x *RepositoryNodeRegistrationAllowlist) GetExpectedHostname() string { + if x != nil { + return x.ExpectedHostname + } + return "" +} + +func (x *RepositoryNodeRegistrationAllowlist) GetExpectedIp() string { + if x != nil { + return x.ExpectedIp + } + return "" +} + +func (x *RepositoryNodeRegistrationAllowlist) GetExpectedArchitecture() RepositoryArchitecture { + if x != nil { + return x.ExpectedArchitecture + } + return RepositoryArchitecture_REPOSITORY_ARCHITECTURE_UNSPECIFIED +} + +func (x *RepositoryNodeRegistrationAllowlist) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *RepositoryNodeRegistrationAllowlist) GetExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.ExpiresAt + } + return nil +} + +func (x *RepositoryNodeRegistrationAllowlist) GetCreatedBy() string { + if x != nil { + return x.CreatedBy + } + return "" +} + +func (x *RepositoryNodeRegistrationAllowlist) GetUpdatedBy() string { + if x != nil { + return x.UpdatedBy + } + return "" +} + +func (x *RepositoryNodeRegistrationAllowlist) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *RepositoryNodeRegistrationAllowlist) GetUpdatedAt() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedAt + } + return nil +} + +type UpsertRepositoryNodeRegistrationAllowlistRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Spec *RepositoryNodeRegistrationAllowlistSpec `protobuf:"bytes,1,opt,name=spec,proto3" json:"spec,omitempty"` +} + +func (x *UpsertRepositoryNodeRegistrationAllowlistRequest) Reset() { + *x = UpsertRepositoryNodeRegistrationAllowlistRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[44] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UpsertRepositoryNodeRegistrationAllowlistRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpsertRepositoryNodeRegistrationAllowlistRequest) ProtoMessage() {} + +func (x *UpsertRepositoryNodeRegistrationAllowlistRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[44] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpsertRepositoryNodeRegistrationAllowlistRequest.ProtoReflect.Descriptor instead. +func (*UpsertRepositoryNodeRegistrationAllowlistRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{44} +} + +func (x *UpsertRepositoryNodeRegistrationAllowlistRequest) GetSpec() *RepositoryNodeRegistrationAllowlistSpec { + if x != nil { + return x.Spec + } + return nil +} + +type RepositoryNodeRegistrationAllowlistResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Allowlist *RepositoryNodeRegistrationAllowlist `protobuf:"bytes,1,opt,name=allowlist,proto3" json:"allowlist,omitempty"` +} + +func (x *RepositoryNodeRegistrationAllowlistResponse) Reset() { + *x = RepositoryNodeRegistrationAllowlistResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[45] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryNodeRegistrationAllowlistResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryNodeRegistrationAllowlistResponse) ProtoMessage() {} + +func (x *RepositoryNodeRegistrationAllowlistResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[45] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryNodeRegistrationAllowlistResponse.ProtoReflect.Descriptor instead. +func (*RepositoryNodeRegistrationAllowlistResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{45} +} + +func (x *RepositoryNodeRegistrationAllowlistResponse) GetAllowlist() *RepositoryNodeRegistrationAllowlist { + if x != nil { + return x.Allowlist + } + return nil +} + +type ListRepositoryNodeRegistrationAllowlistsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ProjectId int64 `protobuf:"varint,1,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` +} + +func (x *ListRepositoryNodeRegistrationAllowlistsRequest) Reset() { + *x = ListRepositoryNodeRegistrationAllowlistsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[46] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListRepositoryNodeRegistrationAllowlistsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRepositoryNodeRegistrationAllowlistsRequest) ProtoMessage() {} + +func (x *ListRepositoryNodeRegistrationAllowlistsRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[46] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRepositoryNodeRegistrationAllowlistsRequest.ProtoReflect.Descriptor instead. +func (*ListRepositoryNodeRegistrationAllowlistsRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{46} +} + +func (x *ListRepositoryNodeRegistrationAllowlistsRequest) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +type ListRepositoryNodeRegistrationAllowlistsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Allowlists []*RepositoryNodeRegistrationAllowlist `protobuf:"bytes,1,rep,name=allowlists,proto3" json:"allowlists,omitempty"` +} + +func (x *ListRepositoryNodeRegistrationAllowlistsResponse) Reset() { + *x = ListRepositoryNodeRegistrationAllowlistsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[47] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListRepositoryNodeRegistrationAllowlistsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRepositoryNodeRegistrationAllowlistsResponse) ProtoMessage() {} + +func (x *ListRepositoryNodeRegistrationAllowlistsResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[47] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRepositoryNodeRegistrationAllowlistsResponse.ProtoReflect.Descriptor instead. +func (*ListRepositoryNodeRegistrationAllowlistsResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{47} +} + +func (x *ListRepositoryNodeRegistrationAllowlistsResponse) GetAllowlists() []*RepositoryNodeRegistrationAllowlist { + if x != nil { + return x.Allowlists + } + return nil +} + +type RepositoryPreflightRecord struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + StorageNodeId string `protobuf:"bytes,2,opt,name=storage_node_id,json=storageNodeId,proto3" json:"storage_node_id,omitempty"` + RepositoryId string `protobuf:"bytes,3,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + TaskId string `protobuf:"bytes,4,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + Status string `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"` + FilesystemType string `protobuf:"bytes,6,opt,name=filesystem_type,json=filesystemType,proto3" json:"filesystem_type,omitempty"` + CapacityBytes int64 `protobuf:"varint,7,opt,name=capacity_bytes,json=capacityBytes,proto3" json:"capacity_bytes,omitempty"` + FreeBytes int64 `protobuf:"varint,8,opt,name=free_bytes,json=freeBytes,proto3" json:"free_bytes,omitempty"` + RequiredFreeBytes int64 `protobuf:"varint,9,opt,name=required_free_bytes,json=requiredFreeBytes,proto3" json:"required_free_bytes,omitempty"` + FreeInodes int64 `protobuf:"varint,10,opt,name=free_inodes,json=freeInodes,proto3" json:"free_inodes,omitempty"` + RequiredInodes int64 `protobuf:"varint,11,opt,name=required_inodes,json=requiredInodes,proto3" json:"required_inodes,omitempty"` + EstimatedPackageCount int64 `protobuf:"varint,12,opt,name=estimated_package_count,json=estimatedPackageCount,proto3" json:"estimated_package_count,omitempty"` + EstimatedImportBytes int64 `protobuf:"varint,13,opt,name=estimated_import_bytes,json=estimatedImportBytes,proto3" json:"estimated_import_bytes,omitempty"` + ToolVersions map[string]string `protobuf:"bytes,14,rep,name=tool_versions,json=toolVersions,proto3" json:"tool_versions,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + ErrorCode string `protobuf:"bytes,15,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"` + ErrorMessage string `protobuf:"bytes,16,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` + ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,17,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,18,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` +} + +func (x *RepositoryPreflightRecord) Reset() { + *x = RepositoryPreflightRecord{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[48] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryPreflightRecord) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryPreflightRecord) ProtoMessage() {} + +func (x *RepositoryPreflightRecord) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[48] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryPreflightRecord.ProtoReflect.Descriptor instead. +func (*RepositoryPreflightRecord) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{48} +} + +func (x *RepositoryPreflightRecord) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *RepositoryPreflightRecord) GetStorageNodeId() string { + if x != nil { + return x.StorageNodeId + } + return "" +} + +func (x *RepositoryPreflightRecord) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *RepositoryPreflightRecord) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *RepositoryPreflightRecord) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *RepositoryPreflightRecord) GetFilesystemType() string { + if x != nil { + return x.FilesystemType + } + return "" +} + +func (x *RepositoryPreflightRecord) GetCapacityBytes() int64 { + if x != nil { + return x.CapacityBytes + } + return 0 +} + +func (x *RepositoryPreflightRecord) GetFreeBytes() int64 { + if x != nil { + return x.FreeBytes + } + return 0 +} + +func (x *RepositoryPreflightRecord) GetRequiredFreeBytes() int64 { + if x != nil { + return x.RequiredFreeBytes + } + return 0 +} + +func (x *RepositoryPreflightRecord) GetFreeInodes() int64 { + if x != nil { + return x.FreeInodes + } + return 0 +} + +func (x *RepositoryPreflightRecord) GetRequiredInodes() int64 { + if x != nil { + return x.RequiredInodes + } + return 0 +} + +func (x *RepositoryPreflightRecord) GetEstimatedPackageCount() int64 { + if x != nil { + return x.EstimatedPackageCount + } + return 0 +} + +func (x *RepositoryPreflightRecord) GetEstimatedImportBytes() int64 { + if x != nil { + return x.EstimatedImportBytes + } + return 0 +} + +func (x *RepositoryPreflightRecord) GetToolVersions() map[string]string { + if x != nil { + return x.ToolVersions + } + return nil +} + +func (x *RepositoryPreflightRecord) GetErrorCode() string { + if x != nil { + return x.ErrorCode + } + return "" +} + +func (x *RepositoryPreflightRecord) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +func (x *RepositoryPreflightRecord) GetExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.ExpiresAt + } + return nil +} + +func (x *RepositoryPreflightRecord) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +type RepositoryStorageNodeProbeResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + StorageNode *RepositoryStorageNode `protobuf:"bytes,1,opt,name=storage_node,json=storageNode,proto3" json:"storage_node,omitempty"` + Preflight *RepositoryPreflightRecord `protobuf:"bytes,2,opt,name=preflight,proto3" json:"preflight,omitempty"` +} + +func (x *RepositoryStorageNodeProbeResponse) Reset() { + *x = RepositoryStorageNodeProbeResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[49] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryStorageNodeProbeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryStorageNodeProbeResponse) ProtoMessage() {} + +func (x *RepositoryStorageNodeProbeResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[49] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryStorageNodeProbeResponse.ProtoReflect.Descriptor instead. +func (*RepositoryStorageNodeProbeResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{49} +} + +func (x *RepositoryStorageNodeProbeResponse) GetStorageNode() *RepositoryStorageNode { + if x != nil { + return x.StorageNode + } + return nil +} + +func (x *RepositoryStorageNodeProbeResponse) GetPreflight() *RepositoryPreflightRecord { + if x != nil { + return x.Preflight + } + return nil +} + +type ListRepositoryPreflightRecordsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + StorageNodeId string `protobuf:"bytes,1,opt,name=storage_node_id,json=storageNodeId,proto3" json:"storage_node_id,omitempty"` + RepositoryId string `protobuf:"bytes,2,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + PageRequest *common.PageRequest `protobuf:"bytes,10,opt,name=page_request,json=pageRequest,proto3" json:"page_request,omitempty"` +} + +func (x *ListRepositoryPreflightRecordsRequest) Reset() { + *x = ListRepositoryPreflightRecordsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[50] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListRepositoryPreflightRecordsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRepositoryPreflightRecordsRequest) ProtoMessage() {} + +func (x *ListRepositoryPreflightRecordsRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[50] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRepositoryPreflightRecordsRequest.ProtoReflect.Descriptor instead. +func (*ListRepositoryPreflightRecordsRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{50} +} + +func (x *ListRepositoryPreflightRecordsRequest) GetStorageNodeId() string { + if x != nil { + return x.StorageNodeId + } + return "" +} + +func (x *ListRepositoryPreflightRecordsRequest) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *ListRepositoryPreflightRecordsRequest) GetPageRequest() *common.PageRequest { + if x != nil { + return x.PageRequest + } + return nil +} + +type ListRepositoryPreflightRecordsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Records []*RepositoryPreflightRecord `protobuf:"bytes,1,rep,name=records,proto3" json:"records,omitempty"` + PageResponse *common.PageResponse `protobuf:"bytes,10,opt,name=page_response,json=pageResponse,proto3" json:"page_response,omitempty"` +} + +func (x *ListRepositoryPreflightRecordsResponse) Reset() { + *x = ListRepositoryPreflightRecordsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[51] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListRepositoryPreflightRecordsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRepositoryPreflightRecordsResponse) ProtoMessage() {} + +func (x *ListRepositoryPreflightRecordsResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[51] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRepositoryPreflightRecordsResponse.ProtoReflect.Descriptor instead. +func (*ListRepositoryPreflightRecordsResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{51} +} + +func (x *ListRepositoryPreflightRecordsResponse) GetRecords() []*RepositoryPreflightRecord { + if x != nil { + return x.Records + } + return nil +} + +func (x *ListRepositoryPreflightRecordsResponse) GetPageResponse() *common.PageResponse { + if x != nil { + return x.PageResponse + } + return nil +} + +type SyncRepositoryRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepositoryId string `protobuf:"bytes,1,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` +} + +func (x *SyncRepositoryRequest) Reset() { + *x = SyncRepositoryRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[52] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SyncRepositoryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SyncRepositoryRequest) ProtoMessage() {} + +func (x *SyncRepositoryRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[52] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SyncRepositoryRequest.ProtoReflect.Descriptor instead. +func (*SyncRepositoryRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{52} +} + +func (x *SyncRepositoryRequest) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +type RepositorySyncScheduleSpec struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Cadence RepositorySyncScheduleCadence `protobuf:"varint,1,opt,name=cadence,proto3,enum=trpc.ocmanager.repository.RepositorySyncScheduleCadence" json:"cadence,omitempty"` + Timezone string `protobuf:"bytes,2,opt,name=timezone,proto3" json:"timezone,omitempty"` + DayOfWeek int32 `protobuf:"varint,3,opt,name=day_of_week,json=dayOfWeek,proto3" json:"day_of_week,omitempty"` + Hour int32 `protobuf:"varint,4,opt,name=hour,proto3" json:"hour,omitempty"` + Minute int32 `protobuf:"varint,5,opt,name=minute,proto3" json:"minute,omitempty"` + FailureThreshold int32 `protobuf:"varint,6,opt,name=failure_threshold,json=failureThreshold,proto3" json:"failure_threshold,omitempty"` +} + +func (x *RepositorySyncScheduleSpec) Reset() { + *x = RepositorySyncScheduleSpec{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[53] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositorySyncScheduleSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositorySyncScheduleSpec) ProtoMessage() {} + +func (x *RepositorySyncScheduleSpec) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[53] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositorySyncScheduleSpec.ProtoReflect.Descriptor instead. +func (*RepositorySyncScheduleSpec) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{53} +} + +func (x *RepositorySyncScheduleSpec) GetCadence() RepositorySyncScheduleCadence { + if x != nil { + return x.Cadence + } + return RepositorySyncScheduleCadence_REPOSITORY_SYNC_SCHEDULE_CADENCE_UNSPECIFIED +} + +func (x *RepositorySyncScheduleSpec) GetTimezone() string { + if x != nil { + return x.Timezone + } + return "" +} + +func (x *RepositorySyncScheduleSpec) GetDayOfWeek() int32 { + if x != nil { + return x.DayOfWeek + } + return 0 +} + +func (x *RepositorySyncScheduleSpec) GetHour() int32 { + if x != nil { + return x.Hour + } + return 0 +} + +func (x *RepositorySyncScheduleSpec) GetMinute() int32 { + if x != nil { + return x.Minute + } + return 0 +} + +func (x *RepositorySyncScheduleSpec) GetFailureThreshold() int32 { + if x != nil { + return x.FailureThreshold + } + return 0 +} + +type RepositorySyncSchedule struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + RepositoryId string `protobuf:"bytes,2,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + ProjectId int64 `protobuf:"varint,3,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + Cadence RepositorySyncScheduleCadence `protobuf:"varint,4,opt,name=cadence,proto3,enum=trpc.ocmanager.repository.RepositorySyncScheduleCadence" json:"cadence,omitempty"` + Timezone string `protobuf:"bytes,5,opt,name=timezone,proto3" json:"timezone,omitempty"` + DayOfWeek int32 `protobuf:"varint,6,opt,name=day_of_week,json=dayOfWeek,proto3" json:"day_of_week,omitempty"` + Hour int32 `protobuf:"varint,7,opt,name=hour,proto3" json:"hour,omitempty"` + Minute int32 `protobuf:"varint,8,opt,name=minute,proto3" json:"minute,omitempty"` + Status RepositorySyncScheduleStatus `protobuf:"varint,9,opt,name=status,proto3,enum=trpc.ocmanager.repository.RepositorySyncScheduleStatus" json:"status,omitempty"` + LastRunAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=last_run_at,json=lastRunAt,proto3" json:"last_run_at,omitempty"` + NextRunAt *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=next_run_at,json=nextRunAt,proto3" json:"next_run_at,omitempty"` + ConsecutiveFailures int32 `protobuf:"varint,12,opt,name=consecutive_failures,json=consecutiveFailures,proto3" json:"consecutive_failures,omitempty"` + FailureThreshold int32 `protobuf:"varint,13,opt,name=failure_threshold,json=failureThreshold,proto3" json:"failure_threshold,omitempty"` + LastErrorCode string `protobuf:"bytes,14,opt,name=last_error_code,json=lastErrorCode,proto3" json:"last_error_code,omitempty"` + LastErrorMessage string `protobuf:"bytes,15,opt,name=last_error_message,json=lastErrorMessage,proto3" json:"last_error_message,omitempty"` + CreatedBy string `protobuf:"bytes,16,opt,name=created_by,json=createdBy,proto3" json:"created_by,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,17,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,18,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` +} + +func (x *RepositorySyncSchedule) Reset() { + *x = RepositorySyncSchedule{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[54] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositorySyncSchedule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositorySyncSchedule) ProtoMessage() {} + +func (x *RepositorySyncSchedule) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[54] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositorySyncSchedule.ProtoReflect.Descriptor instead. +func (*RepositorySyncSchedule) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{54} +} + +func (x *RepositorySyncSchedule) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *RepositorySyncSchedule) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *RepositorySyncSchedule) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +func (x *RepositorySyncSchedule) GetCadence() RepositorySyncScheduleCadence { + if x != nil { + return x.Cadence + } + return RepositorySyncScheduleCadence_REPOSITORY_SYNC_SCHEDULE_CADENCE_UNSPECIFIED +} + +func (x *RepositorySyncSchedule) GetTimezone() string { + if x != nil { + return x.Timezone + } + return "" +} + +func (x *RepositorySyncSchedule) GetDayOfWeek() int32 { + if x != nil { + return x.DayOfWeek + } + return 0 +} + +func (x *RepositorySyncSchedule) GetHour() int32 { + if x != nil { + return x.Hour + } + return 0 +} + +func (x *RepositorySyncSchedule) GetMinute() int32 { + if x != nil { + return x.Minute + } + return 0 +} + +func (x *RepositorySyncSchedule) GetStatus() RepositorySyncScheduleStatus { + if x != nil { + return x.Status + } + return RepositorySyncScheduleStatus_REPOSITORY_SYNC_SCHEDULE_STATUS_UNSPECIFIED +} + +func (x *RepositorySyncSchedule) GetLastRunAt() *timestamppb.Timestamp { + if x != nil { + return x.LastRunAt + } + return nil +} + +func (x *RepositorySyncSchedule) GetNextRunAt() *timestamppb.Timestamp { + if x != nil { + return x.NextRunAt + } + return nil +} + +func (x *RepositorySyncSchedule) GetConsecutiveFailures() int32 { + if x != nil { + return x.ConsecutiveFailures + } + return 0 +} + +func (x *RepositorySyncSchedule) GetFailureThreshold() int32 { + if x != nil { + return x.FailureThreshold + } + return 0 +} + +func (x *RepositorySyncSchedule) GetLastErrorCode() string { + if x != nil { + return x.LastErrorCode + } + return "" +} + +func (x *RepositorySyncSchedule) GetLastErrorMessage() string { + if x != nil { + return x.LastErrorMessage + } + return "" +} + +func (x *RepositorySyncSchedule) GetCreatedBy() string { + if x != nil { + return x.CreatedBy + } + return "" +} + +func (x *RepositorySyncSchedule) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *RepositorySyncSchedule) GetUpdatedAt() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedAt + } + return nil +} + +type UpsertRepositorySyncScheduleRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepositoryId string `protobuf:"bytes,1,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + Spec *RepositorySyncScheduleSpec `protobuf:"bytes,2,opt,name=spec,proto3" json:"spec,omitempty"` +} + +func (x *UpsertRepositorySyncScheduleRequest) Reset() { + *x = UpsertRepositorySyncScheduleRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[55] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UpsertRepositorySyncScheduleRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpsertRepositorySyncScheduleRequest) ProtoMessage() {} + +func (x *UpsertRepositorySyncScheduleRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[55] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpsertRepositorySyncScheduleRequest.ProtoReflect.Descriptor instead. +func (*UpsertRepositorySyncScheduleRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{55} +} + +func (x *UpsertRepositorySyncScheduleRequest) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *UpsertRepositorySyncScheduleRequest) GetSpec() *RepositorySyncScheduleSpec { + if x != nil { + return x.Spec + } + return nil +} + +type DescribeRepositorySyncScheduleRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepositoryId string `protobuf:"bytes,1,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` +} + +func (x *DescribeRepositorySyncScheduleRequest) Reset() { + *x = DescribeRepositorySyncScheduleRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[56] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DescribeRepositorySyncScheduleRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeRepositorySyncScheduleRequest) ProtoMessage() {} + +func (x *DescribeRepositorySyncScheduleRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[56] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeRepositorySyncScheduleRequest.ProtoReflect.Descriptor instead. +func (*DescribeRepositorySyncScheduleRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{56} +} + +func (x *DescribeRepositorySyncScheduleRequest) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +type ChangeRepositorySyncScheduleStatusRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepositoryId string `protobuf:"bytes,1,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` +} + +func (x *ChangeRepositorySyncScheduleStatusRequest) Reset() { + *x = ChangeRepositorySyncScheduleStatusRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[57] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ChangeRepositorySyncScheduleStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChangeRepositorySyncScheduleStatusRequest) ProtoMessage() {} + +func (x *ChangeRepositorySyncScheduleStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[57] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChangeRepositorySyncScheduleStatusRequest.ProtoReflect.Descriptor instead. +func (*ChangeRepositorySyncScheduleStatusRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{57} +} + +func (x *ChangeRepositorySyncScheduleStatusRequest) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +type RepositorySyncScheduleResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Schedule *RepositorySyncSchedule `protobuf:"bytes,1,opt,name=schedule,proto3" json:"schedule,omitempty"` +} + +func (x *RepositorySyncScheduleResponse) Reset() { + *x = RepositorySyncScheduleResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[58] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositorySyncScheduleResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositorySyncScheduleResponse) ProtoMessage() {} + +func (x *RepositorySyncScheduleResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[58] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositorySyncScheduleResponse.ProtoReflect.Descriptor instead. +func (*RepositorySyncScheduleResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{58} +} + +func (x *RepositorySyncScheduleResponse) GetSchedule() *RepositorySyncSchedule { + if x != nil { + return x.Schedule + } + return nil +} + +type RepositoryTask struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + RootTaskId string `protobuf:"bytes,2,opt,name=root_task_id,json=rootTaskId,proto3" json:"root_task_id,omitempty"` + RetryOfTaskId string `protobuf:"bytes,3,opt,name=retry_of_task_id,json=retryOfTaskId,proto3" json:"retry_of_task_id,omitempty"` + RepositoryId string `protobuf:"bytes,4,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + ProjectId int64 `protobuf:"varint,5,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + RevisionId string `protobuf:"bytes,6,opt,name=revision_id,json=revisionId,proto3" json:"revision_id,omitempty"` + Type RepositoryTaskType `protobuf:"varint,7,opt,name=type,proto3,enum=trpc.ocmanager.repository.RepositoryTaskType" json:"type,omitempty"` + Status RepositoryTaskStatus `protobuf:"varint,8,opt,name=status,proto3,enum=trpc.ocmanager.repository.RepositoryTaskStatus" json:"status,omitempty"` + RequiresMutationLock bool `protobuf:"varint,9,opt,name=requires_mutation_lock,json=requiresMutationLock,proto3" json:"requires_mutation_lock,omitempty"` + Attempt int32 `protobuf:"varint,10,opt,name=attempt,proto3" json:"attempt,omitempty"` + MaxAttempts int32 `protobuf:"varint,11,opt,name=max_attempts,json=maxAttempts,proto3" json:"max_attempts,omitempty"` + RequestedBy string `protobuf:"bytes,12,opt,name=requested_by,json=requestedBy,proto3" json:"requested_by,omitempty"` + WorkerId string `protobuf:"bytes,13,opt,name=worker_id,json=workerId,proto3" json:"worker_id,omitempty"` + FencingToken uint64 `protobuf:"varint,14,opt,name=fencing_token,json=fencingToken,proto3" json:"fencing_token,omitempty"` + LeaseExpiresAt *timestamppb.Timestamp `protobuf:"bytes,15,opt,name=lease_expires_at,json=leaseExpiresAt,proto3" json:"lease_expires_at,omitempty"` + LastHeartbeatAt *timestamppb.Timestamp `protobuf:"bytes,16,opt,name=last_heartbeat_at,json=lastHeartbeatAt,proto3" json:"last_heartbeat_at,omitempty"` + AvailableAt *timestamppb.Timestamp `protobuf:"bytes,17,opt,name=available_at,json=availableAt,proto3" json:"available_at,omitempty"` + StartedAt *timestamppb.Timestamp `protobuf:"bytes,18,opt,name=started_at,json=startedAt,proto3" json:"started_at,omitempty"` + FinishedAt *timestamppb.Timestamp `protobuf:"bytes,19,opt,name=finished_at,json=finishedAt,proto3" json:"finished_at,omitempty"` + CancelRequestedAt *timestamppb.Timestamp `protobuf:"bytes,20,opt,name=cancel_requested_at,json=cancelRequestedAt,proto3" json:"cancel_requested_at,omitempty"` + ProgressPercent int32 `protobuf:"varint,21,opt,name=progress_percent,json=progressPercent,proto3" json:"progress_percent,omitempty"` + CurrentStep string `protobuf:"bytes,22,opt,name=current_step,json=currentStep,proto3" json:"current_step,omitempty"` + ErrorCode string `protobuf:"bytes,23,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"` + ErrorMessage string `protobuf:"bytes,24,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,25,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,26,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` + Diagnosis *RepositoryDiagnosis `protobuf:"bytes,27,opt,name=diagnosis,proto3" json:"diagnosis,omitempty"` + StorageNodeId string `protobuf:"bytes,28,opt,name=storage_node_id,json=storageNodeId,proto3" json:"storage_node_id,omitempty"` + PreflightId string `protobuf:"bytes,29,opt,name=preflight_id,json=preflightId,proto3" json:"preflight_id,omitempty"` + CommitState string `protobuf:"bytes,30,opt,name=commit_state,json=commitState,proto3" json:"commit_state,omitempty"` + CommitToken string `protobuf:"bytes,31,opt,name=commit_token,json=commitToken,proto3" json:"commit_token,omitempty"` + PreparedRevisionId string `protobuf:"bytes,32,opt,name=prepared_revision_id,json=preparedRevisionId,proto3" json:"prepared_revision_id,omitempty"` + PreparedManifestSha256 string `protobuf:"bytes,33,opt,name=prepared_manifest_sha256,json=preparedManifestSha256,proto3" json:"prepared_manifest_sha256,omitempty"` + PreparedAt *timestamppb.Timestamp `protobuf:"bytes,34,opt,name=prepared_at,json=preparedAt,proto3" json:"prepared_at,omitempty"` + ActivatedAt *timestamppb.Timestamp `protobuf:"bytes,35,opt,name=activated_at,json=activatedAt,proto3" json:"activated_at,omitempty"` + ConfirmedAt *timestamppb.Timestamp `protobuf:"bytes,36,opt,name=confirmed_at,json=confirmedAt,proto3" json:"confirmed_at,omitempty"` + ResultObjectId string `protobuf:"bytes,37,opt,name=result_object_id,json=resultObjectId,proto3" json:"result_object_id,omitempty"` + ResultObjectSha256 string `protobuf:"bytes,38,opt,name=result_object_sha256,json=resultObjectSha256,proto3" json:"result_object_sha256,omitempty"` + RepositoryName string `protobuf:"bytes,39,opt,name=repository_name,json=repositoryName,proto3" json:"repository_name,omitempty"` + StorageNodeName string `protobuf:"bytes,40,opt,name=storage_node_name,json=storageNodeName,proto3" json:"storage_node_name,omitempty"` + Trigger string `protobuf:"bytes,41,opt,name=trigger,proto3" json:"trigger,omitempty"` + IdempotencyKey string `protobuf:"bytes,42,opt,name=idempotency_key,json=idempotencyKey,proto3" json:"idempotency_key,omitempty"` + BandwidthLimitBytes int64 `protobuf:"varint,43,opt,name=bandwidth_limit_bytes,json=bandwidthLimitBytes,proto3" json:"bandwidth_limit_bytes,omitempty"` + MaxParallelDownloads int32 `protobuf:"varint,44,opt,name=max_parallel_downloads,json=maxParallelDownloads,proto3" json:"max_parallel_downloads,omitempty"` + ScopeType string `protobuf:"bytes,45,opt,name=scope_type,json=scopeType,proto3" json:"scope_type,omitempty"` + ScopeId string `protobuf:"bytes,46,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty"` + ExecutorKind string `protobuf:"bytes,47,opt,name=executor_kind,json=executorKind,proto3" json:"executor_kind,omitempty"` + Targets []*RepositoryTaskTarget `protobuf:"bytes,48,rep,name=targets,proto3" json:"targets,omitempty"` + ErrorZhMessage string `protobuf:"bytes,49,opt,name=error_zh_message,json=errorZhMessage,proto3" json:"error_zh_message,omitempty"` + ErrorZhRecommendedAction string `protobuf:"bytes,50,opt,name=error_zh_recommended_action,json=errorZhRecommendedAction,proto3" json:"error_zh_recommended_action,omitempty"` + ResultItems []*RepositoryTaskResultItem `protobuf:"bytes,51,rep,name=result_items,json=resultItems,proto3" json:"result_items,omitempty"` +} + +func (x *RepositoryTask) Reset() { + *x = RepositoryTask{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[59] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryTask) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryTask) ProtoMessage() {} + +func (x *RepositoryTask) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[59] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryTask.ProtoReflect.Descriptor instead. +func (*RepositoryTask) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{59} +} + +func (x *RepositoryTask) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *RepositoryTask) GetRootTaskId() string { + if x != nil { + return x.RootTaskId + } + return "" +} + +func (x *RepositoryTask) GetRetryOfTaskId() string { + if x != nil { + return x.RetryOfTaskId + } + return "" +} + +func (x *RepositoryTask) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *RepositoryTask) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +func (x *RepositoryTask) GetRevisionId() string { + if x != nil { + return x.RevisionId + } + return "" +} + +func (x *RepositoryTask) GetType() RepositoryTaskType { + if x != nil { + return x.Type + } + return RepositoryTaskType_REPOSITORY_TASK_TYPE_UNSPECIFIED +} + +func (x *RepositoryTask) GetStatus() RepositoryTaskStatus { + if x != nil { + return x.Status + } + return RepositoryTaskStatus_REPOSITORY_TASK_STATUS_UNSPECIFIED +} + +func (x *RepositoryTask) GetRequiresMutationLock() bool { + if x != nil { + return x.RequiresMutationLock + } + return false +} + +func (x *RepositoryTask) GetAttempt() int32 { + if x != nil { + return x.Attempt + } + return 0 +} + +func (x *RepositoryTask) GetMaxAttempts() int32 { + if x != nil { + return x.MaxAttempts + } + return 0 +} + +func (x *RepositoryTask) GetRequestedBy() string { + if x != nil { + return x.RequestedBy + } + return "" +} + +func (x *RepositoryTask) GetWorkerId() string { + if x != nil { + return x.WorkerId + } + return "" +} + +func (x *RepositoryTask) GetFencingToken() uint64 { + if x != nil { + return x.FencingToken + } + return 0 +} + +func (x *RepositoryTask) GetLeaseExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.LeaseExpiresAt + } + return nil +} + +func (x *RepositoryTask) GetLastHeartbeatAt() *timestamppb.Timestamp { + if x != nil { + return x.LastHeartbeatAt + } + return nil +} + +func (x *RepositoryTask) GetAvailableAt() *timestamppb.Timestamp { + if x != nil { + return x.AvailableAt + } + return nil +} + +func (x *RepositoryTask) GetStartedAt() *timestamppb.Timestamp { + if x != nil { + return x.StartedAt + } + return nil +} + +func (x *RepositoryTask) GetFinishedAt() *timestamppb.Timestamp { + if x != nil { + return x.FinishedAt + } + return nil +} + +func (x *RepositoryTask) GetCancelRequestedAt() *timestamppb.Timestamp { + if x != nil { + return x.CancelRequestedAt + } + return nil +} + +func (x *RepositoryTask) GetProgressPercent() int32 { + if x != nil { + return x.ProgressPercent + } + return 0 +} + +func (x *RepositoryTask) GetCurrentStep() string { + if x != nil { + return x.CurrentStep + } + return "" +} + +func (x *RepositoryTask) GetErrorCode() string { + if x != nil { + return x.ErrorCode + } + return "" +} + +func (x *RepositoryTask) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +func (x *RepositoryTask) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *RepositoryTask) GetUpdatedAt() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedAt + } + return nil +} + +func (x *RepositoryTask) GetDiagnosis() *RepositoryDiagnosis { + if x != nil { + return x.Diagnosis + } + return nil +} + +func (x *RepositoryTask) GetStorageNodeId() string { + if x != nil { + return x.StorageNodeId + } + return "" +} + +func (x *RepositoryTask) GetPreflightId() string { + if x != nil { + return x.PreflightId + } + return "" +} + +func (x *RepositoryTask) GetCommitState() string { + if x != nil { + return x.CommitState + } + return "" +} + +func (x *RepositoryTask) GetCommitToken() string { + if x != nil { + return x.CommitToken + } + return "" +} + +func (x *RepositoryTask) GetPreparedRevisionId() string { + if x != nil { + return x.PreparedRevisionId + } + return "" +} + +func (x *RepositoryTask) GetPreparedManifestSha256() string { + if x != nil { + return x.PreparedManifestSha256 + } + return "" +} + +func (x *RepositoryTask) GetPreparedAt() *timestamppb.Timestamp { + if x != nil { + return x.PreparedAt + } + return nil +} + +func (x *RepositoryTask) GetActivatedAt() *timestamppb.Timestamp { + if x != nil { + return x.ActivatedAt + } + return nil +} + +func (x *RepositoryTask) GetConfirmedAt() *timestamppb.Timestamp { + if x != nil { + return x.ConfirmedAt + } + return nil +} + +func (x *RepositoryTask) GetResultObjectId() string { + if x != nil { + return x.ResultObjectId + } + return "" +} + +func (x *RepositoryTask) GetResultObjectSha256() string { + if x != nil { + return x.ResultObjectSha256 + } + return "" +} + +func (x *RepositoryTask) GetRepositoryName() string { + if x != nil { + return x.RepositoryName + } + return "" +} + +func (x *RepositoryTask) GetStorageNodeName() string { + if x != nil { + return x.StorageNodeName + } + return "" +} + +func (x *RepositoryTask) GetTrigger() string { + if x != nil { + return x.Trigger + } + return "" +} + +func (x *RepositoryTask) GetIdempotencyKey() string { + if x != nil { + return x.IdempotencyKey + } + return "" +} + +func (x *RepositoryTask) GetBandwidthLimitBytes() int64 { + if x != nil { + return x.BandwidthLimitBytes + } + return 0 +} + +func (x *RepositoryTask) GetMaxParallelDownloads() int32 { + if x != nil { + return x.MaxParallelDownloads + } + return 0 +} + +func (x *RepositoryTask) GetScopeType() string { + if x != nil { + return x.ScopeType + } + return "" +} + +func (x *RepositoryTask) GetScopeId() string { + if x != nil { + return x.ScopeId + } + return "" +} + +func (x *RepositoryTask) GetExecutorKind() string { + if x != nil { + return x.ExecutorKind + } + return "" +} + +func (x *RepositoryTask) GetTargets() []*RepositoryTaskTarget { + if x != nil { + return x.Targets + } + return nil +} + +func (x *RepositoryTask) GetErrorZhMessage() string { + if x != nil { + return x.ErrorZhMessage + } + return "" +} + +func (x *RepositoryTask) GetErrorZhRecommendedAction() string { + if x != nil { + return x.ErrorZhRecommendedAction + } + return "" +} + +func (x *RepositoryTask) GetResultItems() []*RepositoryTaskResultItem { + if x != nil { + return x.ResultItems + } + return nil +} + +type RepositoryTaskResultItem struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + TaskId string `protobuf:"bytes,2,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + ItemKey string `protobuf:"bytes,3,opt,name=item_key,json=itemKey,proto3" json:"item_key,omitempty"` + PackageNevra string `protobuf:"bytes,4,opt,name=package_nevra,json=packageNevra,proto3" json:"package_nevra,omitempty"` + Status RepositoryTaskStatus `protobuf:"varint,5,opt,name=status,proto3,enum=trpc.ocmanager.repository.RepositoryTaskStatus" json:"status,omitempty"` + ErrorCode string `protobuf:"bytes,6,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"` + Message string `protobuf:"bytes,7,opt,name=message,proto3" json:"message,omitempty"` + ResultJson string `protobuf:"bytes,8,opt,name=result_json,json=resultJson,proto3" json:"result_json,omitempty"` + StartedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=started_at,json=startedAt,proto3" json:"started_at,omitempty"` + FinishedAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=finished_at,json=finishedAt,proto3" json:"finished_at,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` +} + +func (x *RepositoryTaskResultItem) Reset() { + *x = RepositoryTaskResultItem{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[60] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryTaskResultItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryTaskResultItem) ProtoMessage() {} + +func (x *RepositoryTaskResultItem) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[60] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryTaskResultItem.ProtoReflect.Descriptor instead. +func (*RepositoryTaskResultItem) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{60} +} + +func (x *RepositoryTaskResultItem) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *RepositoryTaskResultItem) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *RepositoryTaskResultItem) GetItemKey() string { + if x != nil { + return x.ItemKey + } + return "" +} + +func (x *RepositoryTaskResultItem) GetPackageNevra() string { + if x != nil { + return x.PackageNevra + } + return "" +} + +func (x *RepositoryTaskResultItem) GetStatus() RepositoryTaskStatus { + if x != nil { + return x.Status + } + return RepositoryTaskStatus_REPOSITORY_TASK_STATUS_UNSPECIFIED +} + +func (x *RepositoryTaskResultItem) GetErrorCode() string { + if x != nil { + return x.ErrorCode + } + return "" +} + +func (x *RepositoryTaskResultItem) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *RepositoryTaskResultItem) GetResultJson() string { + if x != nil { + return x.ResultJson + } + return "" +} + +func (x *RepositoryTaskResultItem) GetStartedAt() *timestamppb.Timestamp { + if x != nil { + return x.StartedAt + } + return nil +} + +func (x *RepositoryTaskResultItem) GetFinishedAt() *timestamppb.Timestamp { + if x != nil { + return x.FinishedAt + } + return nil +} + +func (x *RepositoryTaskResultItem) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +type RepositoryTaskTarget struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + TaskId string `protobuf:"bytes,2,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + TargetType string `protobuf:"bytes,3,opt,name=target_type,json=targetType,proto3" json:"target_type,omitempty"` + TargetId string `protobuf:"bytes,4,opt,name=target_id,json=targetId,proto3" json:"target_id,omitempty"` + StorageNodeId string `protobuf:"bytes,5,opt,name=storage_node_id,json=storageNodeId,proto3" json:"storage_node_id,omitempty"` + TmsAgentId uint64 `protobuf:"varint,6,opt,name=tms_agent_id,json=tmsAgentId,proto3" json:"tms_agent_id,omitempty"` + Status RepositoryTaskStatus `protobuf:"varint,7,opt,name=status,proto3,enum=trpc.ocmanager.repository.RepositoryTaskStatus" json:"status,omitempty"` + ProgressPercent int32 `protobuf:"varint,8,opt,name=progress_percent,json=progressPercent,proto3" json:"progress_percent,omitempty"` + StartedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=started_at,json=startedAt,proto3" json:"started_at,omitempty"` + FinishedAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=finished_at,json=finishedAt,proto3" json:"finished_at,omitempty"` + ErrorCode string `protobuf:"bytes,11,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"` + ErrorMessage string `protobuf:"bytes,12,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` + ResultJson string `protobuf:"bytes,13,opt,name=result_json,json=resultJson,proto3" json:"result_json,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,14,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,15,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` + ErrorZhMessage string `protobuf:"bytes,16,opt,name=error_zh_message,json=errorZhMessage,proto3" json:"error_zh_message,omitempty"` + ErrorZhRecommendedAction string `protobuf:"bytes,17,opt,name=error_zh_recommended_action,json=errorZhRecommendedAction,proto3" json:"error_zh_recommended_action,omitempty"` +} + +func (x *RepositoryTaskTarget) Reset() { + *x = RepositoryTaskTarget{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[61] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryTaskTarget) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryTaskTarget) ProtoMessage() {} + +func (x *RepositoryTaskTarget) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[61] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryTaskTarget.ProtoReflect.Descriptor instead. +func (*RepositoryTaskTarget) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{61} +} + +func (x *RepositoryTaskTarget) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *RepositoryTaskTarget) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *RepositoryTaskTarget) GetTargetType() string { + if x != nil { + return x.TargetType + } + return "" +} + +func (x *RepositoryTaskTarget) GetTargetId() string { + if x != nil { + return x.TargetId + } + return "" +} + +func (x *RepositoryTaskTarget) GetStorageNodeId() string { + if x != nil { + return x.StorageNodeId + } + return "" +} + +func (x *RepositoryTaskTarget) GetTmsAgentId() uint64 { + if x != nil { + return x.TmsAgentId + } + return 0 +} + +func (x *RepositoryTaskTarget) GetStatus() RepositoryTaskStatus { + if x != nil { + return x.Status + } + return RepositoryTaskStatus_REPOSITORY_TASK_STATUS_UNSPECIFIED +} + +func (x *RepositoryTaskTarget) GetProgressPercent() int32 { + if x != nil { + return x.ProgressPercent + } + return 0 +} + +func (x *RepositoryTaskTarget) GetStartedAt() *timestamppb.Timestamp { + if x != nil { + return x.StartedAt + } + return nil +} + +func (x *RepositoryTaskTarget) GetFinishedAt() *timestamppb.Timestamp { + if x != nil { + return x.FinishedAt + } + return nil +} + +func (x *RepositoryTaskTarget) GetErrorCode() string { + if x != nil { + return x.ErrorCode + } + return "" +} + +func (x *RepositoryTaskTarget) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +func (x *RepositoryTaskTarget) GetResultJson() string { + if x != nil { + return x.ResultJson + } + return "" +} + +func (x *RepositoryTaskTarget) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *RepositoryTaskTarget) GetUpdatedAt() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedAt + } + return nil +} + +func (x *RepositoryTaskTarget) GetErrorZhMessage() string { + if x != nil { + return x.ErrorZhMessage + } + return "" +} + +func (x *RepositoryTaskTarget) GetErrorZhRecommendedAction() string { + if x != nil { + return x.ErrorZhRecommendedAction + } + return "" +} + +type DescribeRepositoryTaskRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` +} + +func (x *DescribeRepositoryTaskRequest) Reset() { + *x = DescribeRepositoryTaskRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[62] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DescribeRepositoryTaskRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeRepositoryTaskRequest) ProtoMessage() {} + +func (x *DescribeRepositoryTaskRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[62] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeRepositoryTaskRequest.ProtoReflect.Descriptor instead. +func (*DescribeRepositoryTaskRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{62} +} + +func (x *DescribeRepositoryTaskRequest) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +type ListRepositoryTasksRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepositoryId string `protobuf:"bytes,1,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + Status RepositoryTaskStatus `protobuf:"varint,2,opt,name=status,proto3,enum=trpc.ocmanager.repository.RepositoryTaskStatus" json:"status,omitempty"` + ProjectId int64 `protobuf:"varint,3,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + StorageNodeId string `protobuf:"bytes,4,opt,name=storage_node_id,json=storageNodeId,proto3" json:"storage_node_id,omitempty"` + Type RepositoryTaskType `protobuf:"varint,5,opt,name=type,proto3,enum=trpc.ocmanager.repository.RepositoryTaskType" json:"type,omitempty"` + Trigger string `protobuf:"bytes,6,opt,name=trigger,proto3" json:"trigger,omitempty"` + CreatedAtFrom *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=created_at_from,json=createdAtFrom,proto3" json:"created_at_from,omitempty"` + CreatedAtTo *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=created_at_to,json=createdAtTo,proto3" json:"created_at_to,omitempty"` + PageRequest *common.PageRequest `protobuf:"bytes,10,opt,name=page_request,json=pageRequest,proto3" json:"page_request,omitempty"` +} + +func (x *ListRepositoryTasksRequest) Reset() { + *x = ListRepositoryTasksRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[63] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListRepositoryTasksRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRepositoryTasksRequest) ProtoMessage() {} + +func (x *ListRepositoryTasksRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[63] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRepositoryTasksRequest.ProtoReflect.Descriptor instead. +func (*ListRepositoryTasksRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{63} +} + +func (x *ListRepositoryTasksRequest) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *ListRepositoryTasksRequest) GetStatus() RepositoryTaskStatus { + if x != nil { + return x.Status + } + return RepositoryTaskStatus_REPOSITORY_TASK_STATUS_UNSPECIFIED +} + +func (x *ListRepositoryTasksRequest) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +func (x *ListRepositoryTasksRequest) GetStorageNodeId() string { + if x != nil { + return x.StorageNodeId + } + return "" +} + +func (x *ListRepositoryTasksRequest) GetType() RepositoryTaskType { + if x != nil { + return x.Type + } + return RepositoryTaskType_REPOSITORY_TASK_TYPE_UNSPECIFIED +} + +func (x *ListRepositoryTasksRequest) GetTrigger() string { + if x != nil { + return x.Trigger + } + return "" +} + +func (x *ListRepositoryTasksRequest) GetCreatedAtFrom() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAtFrom + } + return nil +} + +func (x *ListRepositoryTasksRequest) GetCreatedAtTo() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAtTo + } + return nil +} + +func (x *ListRepositoryTasksRequest) GetPageRequest() *common.PageRequest { + if x != nil { + return x.PageRequest + } + return nil +} + +type RepositoryTaskStatistics struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PendingCount int64 `protobuf:"varint,1,opt,name=pending_count,json=pendingCount,proto3" json:"pending_count,omitempty"` + RunningCount int64 `protobuf:"varint,2,opt,name=running_count,json=runningCount,proto3" json:"running_count,omitempty"` + FailedCount int64 `protobuf:"varint,3,opt,name=failed_count,json=failedCount,proto3" json:"failed_count,omitempty"` + SuccessCount int64 `protobuf:"varint,4,opt,name=success_count,json=successCount,proto3" json:"success_count,omitempty"` +} + +func (x *RepositoryTaskStatistics) Reset() { + *x = RepositoryTaskStatistics{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[64] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryTaskStatistics) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryTaskStatistics) ProtoMessage() {} + +func (x *RepositoryTaskStatistics) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[64] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryTaskStatistics.ProtoReflect.Descriptor instead. +func (*RepositoryTaskStatistics) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{64} +} + +func (x *RepositoryTaskStatistics) GetPendingCount() int64 { + if x != nil { + return x.PendingCount + } + return 0 +} + +func (x *RepositoryTaskStatistics) GetRunningCount() int64 { + if x != nil { + return x.RunningCount + } + return 0 +} + +func (x *RepositoryTaskStatistics) GetFailedCount() int64 { + if x != nil { + return x.FailedCount + } + return 0 +} + +func (x *RepositoryTaskStatistics) GetSuccessCount() int64 { + if x != nil { + return x.SuccessCount + } + return 0 +} + +type CancelRepositoryTaskRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` +} + +func (x *CancelRepositoryTaskRequest) Reset() { + *x = CancelRepositoryTaskRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[65] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CancelRepositoryTaskRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CancelRepositoryTaskRequest) ProtoMessage() {} + +func (x *CancelRepositoryTaskRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[65] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CancelRepositoryTaskRequest.ProtoReflect.Descriptor instead. +func (*CancelRepositoryTaskRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{65} +} + +func (x *CancelRepositoryTaskRequest) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +type RetryRepositoryTaskRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` +} + +func (x *RetryRepositoryTaskRequest) Reset() { + *x = RetryRepositoryTaskRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[66] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RetryRepositoryTaskRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RetryRepositoryTaskRequest) ProtoMessage() {} + +func (x *RetryRepositoryTaskRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[66] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RetryRepositoryTaskRequest.ProtoReflect.Descriptor instead. +func (*RetryRepositoryTaskRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{66} +} + +func (x *RetryRepositoryTaskRequest) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +type RepositoryTaskResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Task *RepositoryTask `protobuf:"bytes,1,opt,name=task,proto3" json:"task,omitempty"` +} + +func (x *RepositoryTaskResponse) Reset() { + *x = RepositoryTaskResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[67] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryTaskResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryTaskResponse) ProtoMessage() {} + +func (x *RepositoryTaskResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[67] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryTaskResponse.ProtoReflect.Descriptor instead. +func (*RepositoryTaskResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{67} +} + +func (x *RepositoryTaskResponse) GetTask() *RepositoryTask { + if x != nil { + return x.Task + } + return nil +} + +type RepositoryOCAIContext struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + RepositoryId string `protobuf:"bytes,2,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + RepositoryName string `protobuf:"bytes,3,opt,name=repository_name,json=repositoryName,proto3" json:"repository_name,omitempty"` + SystemProduct string `protobuf:"bytes,4,opt,name=system_product,json=systemProduct,proto3" json:"system_product,omitempty"` + ProductVersion string `protobuf:"bytes,5,opt,name=product_version,json=productVersion,proto3" json:"product_version,omitempty"` + Architecture string `protobuf:"bytes,6,opt,name=architecture,proto3" json:"architecture,omitempty"` + TaskType string `protobuf:"bytes,7,opt,name=task_type,json=taskType,proto3" json:"task_type,omitempty"` + TaskStatus string `protobuf:"bytes,8,opt,name=task_status,json=taskStatus,proto3" json:"task_status,omitempty"` + StorageNodeId string `protobuf:"bytes,9,opt,name=storage_node_id,json=storageNodeId,proto3" json:"storage_node_id,omitempty"` + RevisionId string `protobuf:"bytes,10,opt,name=revision_id,json=revisionId,proto3" json:"revision_id,omitempty"` + CurrentStep string `protobuf:"bytes,11,opt,name=current_step,json=currentStep,proto3" json:"current_step,omitempty"` + ErrorCode string `protobuf:"bytes,12,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"` + ErrorMessage string `protobuf:"bytes,13,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` + Diagnosis *RepositoryDiagnosis `protobuf:"bytes,14,opt,name=diagnosis,proto3" json:"diagnosis,omitempty"` + LogSummary string `protobuf:"bytes,15,opt,name=log_summary,json=logSummary,proto3" json:"log_summary,omitempty"` + ContextJson string `protobuf:"bytes,16,opt,name=context_json,json=contextJson,proto3" json:"context_json,omitempty"` + ContextSha256 string `protobuf:"bytes,17,opt,name=context_sha256,json=contextSha256,proto3" json:"context_sha256,omitempty"` + Truncated bool `protobuf:"varint,18,opt,name=truncated,proto3" json:"truncated,omitempty"` +} + +func (x *RepositoryOCAIContext) Reset() { + *x = RepositoryOCAIContext{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[68] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryOCAIContext) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryOCAIContext) ProtoMessage() {} + +func (x *RepositoryOCAIContext) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[68] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryOCAIContext.ProtoReflect.Descriptor instead. +func (*RepositoryOCAIContext) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{68} +} + +func (x *RepositoryOCAIContext) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *RepositoryOCAIContext) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *RepositoryOCAIContext) GetRepositoryName() string { + if x != nil { + return x.RepositoryName + } + return "" +} + +func (x *RepositoryOCAIContext) GetSystemProduct() string { + if x != nil { + return x.SystemProduct + } + return "" +} + +func (x *RepositoryOCAIContext) GetProductVersion() string { + if x != nil { + return x.ProductVersion + } + return "" +} + +func (x *RepositoryOCAIContext) GetArchitecture() string { + if x != nil { + return x.Architecture + } + return "" +} + +func (x *RepositoryOCAIContext) GetTaskType() string { + if x != nil { + return x.TaskType + } + return "" +} + +func (x *RepositoryOCAIContext) GetTaskStatus() string { + if x != nil { + return x.TaskStatus + } + return "" +} + +func (x *RepositoryOCAIContext) GetStorageNodeId() string { + if x != nil { + return x.StorageNodeId + } + return "" +} + +func (x *RepositoryOCAIContext) GetRevisionId() string { + if x != nil { + return x.RevisionId + } + return "" +} + +func (x *RepositoryOCAIContext) GetCurrentStep() string { + if x != nil { + return x.CurrentStep + } + return "" +} + +func (x *RepositoryOCAIContext) GetErrorCode() string { + if x != nil { + return x.ErrorCode + } + return "" +} + +func (x *RepositoryOCAIContext) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +func (x *RepositoryOCAIContext) GetDiagnosis() *RepositoryDiagnosis { + if x != nil { + return x.Diagnosis + } + return nil +} + +func (x *RepositoryOCAIContext) GetLogSummary() string { + if x != nil { + return x.LogSummary + } + return "" +} + +func (x *RepositoryOCAIContext) GetContextJson() string { + if x != nil { + return x.ContextJson + } + return "" +} + +func (x *RepositoryOCAIContext) GetContextSha256() string { + if x != nil { + return x.ContextSha256 + } + return "" +} + +func (x *RepositoryOCAIContext) GetTruncated() bool { + if x != nil { + return x.Truncated + } + return false +} + +type RepositoryOCAIAnalysis struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + TaskId string `protobuf:"bytes,2,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + SessionId string `protobuf:"bytes,3,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + ContextSha256 string `protobuf:"bytes,4,opt,name=context_sha256,json=contextSha256,proto3" json:"context_sha256,omitempty"` + Status string `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"` + ActionTaskId string `protobuf:"bytes,6,opt,name=action_task_id,json=actionTaskId,proto3" json:"action_task_id,omitempty"` + ActionResult string `protobuf:"bytes,7,opt,name=action_result,json=actionResult,proto3" json:"action_result,omitempty"` + RequestedBy string `protobuf:"bytes,8,opt,name=requested_by,json=requestedBy,proto3" json:"requested_by,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + FeedbackAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=feedback_at,json=feedbackAt,proto3" json:"feedback_at,omitempty"` +} + +func (x *RepositoryOCAIAnalysis) Reset() { + *x = RepositoryOCAIAnalysis{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[69] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryOCAIAnalysis) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryOCAIAnalysis) ProtoMessage() {} + +func (x *RepositoryOCAIAnalysis) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[69] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryOCAIAnalysis.ProtoReflect.Descriptor instead. +func (*RepositoryOCAIAnalysis) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{69} +} + +func (x *RepositoryOCAIAnalysis) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *RepositoryOCAIAnalysis) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *RepositoryOCAIAnalysis) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *RepositoryOCAIAnalysis) GetContextSha256() string { + if x != nil { + return x.ContextSha256 + } + return "" +} + +func (x *RepositoryOCAIAnalysis) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *RepositoryOCAIAnalysis) GetActionTaskId() string { + if x != nil { + return x.ActionTaskId + } + return "" +} + +func (x *RepositoryOCAIAnalysis) GetActionResult() string { + if x != nil { + return x.ActionResult + } + return "" +} + +func (x *RepositoryOCAIAnalysis) GetRequestedBy() string { + if x != nil { + return x.RequestedBy + } + return "" +} + +func (x *RepositoryOCAIAnalysis) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *RepositoryOCAIAnalysis) GetFeedbackAt() *timestamppb.Timestamp { + if x != nil { + return x.FeedbackAt + } + return nil +} + +type BuildRepositoryOCAIContextRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` +} + +func (x *BuildRepositoryOCAIContextRequest) Reset() { + *x = BuildRepositoryOCAIContextRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[70] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *BuildRepositoryOCAIContextRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BuildRepositoryOCAIContextRequest) ProtoMessage() {} + +func (x *BuildRepositoryOCAIContextRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[70] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BuildRepositoryOCAIContextRequest.ProtoReflect.Descriptor instead. +func (*BuildRepositoryOCAIContextRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{70} +} + +func (x *BuildRepositoryOCAIContextRequest) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +type CreateRepositoryOCAIAnalysisRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` +} + +func (x *CreateRepositoryOCAIAnalysisRequest) Reset() { + *x = CreateRepositoryOCAIAnalysisRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[71] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateRepositoryOCAIAnalysisRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateRepositoryOCAIAnalysisRequest) ProtoMessage() {} + +func (x *CreateRepositoryOCAIAnalysisRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[71] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateRepositoryOCAIAnalysisRequest.ProtoReflect.Descriptor instead. +func (*CreateRepositoryOCAIAnalysisRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{71} +} + +func (x *CreateRepositoryOCAIAnalysisRequest) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +type DescribeRepositoryOCAIAnalysisRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + AnalysisId string `protobuf:"bytes,1,opt,name=analysis_id,json=analysisId,proto3" json:"analysis_id,omitempty"` +} + +func (x *DescribeRepositoryOCAIAnalysisRequest) Reset() { + *x = DescribeRepositoryOCAIAnalysisRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[72] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DescribeRepositoryOCAIAnalysisRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeRepositoryOCAIAnalysisRequest) ProtoMessage() {} + +func (x *DescribeRepositoryOCAIAnalysisRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[72] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeRepositoryOCAIAnalysisRequest.ProtoReflect.Descriptor instead. +func (*DescribeRepositoryOCAIAnalysisRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{72} +} + +func (x *DescribeRepositoryOCAIAnalysisRequest) GetAnalysisId() string { + if x != nil { + return x.AnalysisId + } + return "" +} + +type AppendRepositoryOCAIFeedbackRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + ActionTaskId string `protobuf:"bytes,2,opt,name=action_task_id,json=actionTaskId,proto3" json:"action_task_id,omitempty"` + ActionResult string `protobuf:"bytes,3,opt,name=action_result,json=actionResult,proto3" json:"action_result,omitempty"` +} + +func (x *AppendRepositoryOCAIFeedbackRequest) Reset() { + *x = AppendRepositoryOCAIFeedbackRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[73] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AppendRepositoryOCAIFeedbackRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AppendRepositoryOCAIFeedbackRequest) ProtoMessage() {} + +func (x *AppendRepositoryOCAIFeedbackRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[73] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AppendRepositoryOCAIFeedbackRequest.ProtoReflect.Descriptor instead. +func (*AppendRepositoryOCAIFeedbackRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{73} +} + +func (x *AppendRepositoryOCAIFeedbackRequest) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *AppendRepositoryOCAIFeedbackRequest) GetActionTaskId() string { + if x != nil { + return x.ActionTaskId + } + return "" +} + +func (x *AppendRepositoryOCAIFeedbackRequest) GetActionResult() string { + if x != nil { + return x.ActionResult + } + return "" +} + +type RepositoryOCAIContextResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Context *RepositoryOCAIContext `protobuf:"bytes,1,opt,name=context,proto3" json:"context,omitempty"` +} + +func (x *RepositoryOCAIContextResponse) Reset() { + *x = RepositoryOCAIContextResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[74] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryOCAIContextResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryOCAIContextResponse) ProtoMessage() {} + +func (x *RepositoryOCAIContextResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[74] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryOCAIContextResponse.ProtoReflect.Descriptor instead. +func (*RepositoryOCAIContextResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{74} +} + +func (x *RepositoryOCAIContextResponse) GetContext() *RepositoryOCAIContext { + if x != nil { + return x.Context + } + return nil +} + +type RepositoryOCAIAnalysisResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Analysis *RepositoryOCAIAnalysis `protobuf:"bytes,1,opt,name=analysis,proto3" json:"analysis,omitempty"` + Context *RepositoryOCAIContext `protobuf:"bytes,2,opt,name=context,proto3" json:"context,omitempty"` + Instruction string `protobuf:"bytes,3,opt,name=instruction,proto3" json:"instruction,omitempty"` +} + +func (x *RepositoryOCAIAnalysisResponse) Reset() { + *x = RepositoryOCAIAnalysisResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[75] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryOCAIAnalysisResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryOCAIAnalysisResponse) ProtoMessage() {} + +func (x *RepositoryOCAIAnalysisResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[75] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryOCAIAnalysisResponse.ProtoReflect.Descriptor instead. +func (*RepositoryOCAIAnalysisResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{75} +} + +func (x *RepositoryOCAIAnalysisResponse) GetAnalysis() *RepositoryOCAIAnalysis { + if x != nil { + return x.Analysis + } + return nil +} + +func (x *RepositoryOCAIAnalysisResponse) GetContext() *RepositoryOCAIContext { + if x != nil { + return x.Context + } + return nil +} + +func (x *RepositoryOCAIAnalysisResponse) GetInstruction() string { + if x != nil { + return x.Instruction + } + return "" +} + +type GenerateRepositoryConfigRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepositoryId string `protobuf:"bytes,1,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + ImmutableRevision bool `protobuf:"varint,2,opt,name=immutable_revision,json=immutableRevision,proto3" json:"immutable_revision,omitempty"` +} + +func (x *GenerateRepositoryConfigRequest) Reset() { + *x = GenerateRepositoryConfigRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[76] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GenerateRepositoryConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GenerateRepositoryConfigRequest) ProtoMessage() {} + +func (x *GenerateRepositoryConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[76] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GenerateRepositoryConfigRequest.ProtoReflect.Descriptor instead. +func (*GenerateRepositoryConfigRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{76} +} + +func (x *GenerateRepositoryConfigRequest) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *GenerateRepositoryConfigRequest) GetImmutableRevision() bool { + if x != nil { + return x.ImmutableRevision + } + return false +} + +type RepositoryConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepositoryId string `protobuf:"bytes,1,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + ActiveRevisionId string `protobuf:"bytes,2,opt,name=active_revision_id,json=activeRevisionId,proto3" json:"active_revision_id,omitempty"` + RepoId string `protobuf:"bytes,3,opt,name=repo_id,json=repoId,proto3" json:"repo_id,omitempty"` + FileName string `protobuf:"bytes,4,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"` + ContentType string `protobuf:"bytes,5,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"` + Content string `protobuf:"bytes,6,opt,name=content,proto3" json:"content,omitempty"` + Baseurl string `protobuf:"bytes,7,opt,name=baseurl,proto3" json:"baseurl,omitempty"` + RevisionUrl string `protobuf:"bytes,8,opt,name=revision_url,json=revisionUrl,proto3" json:"revision_url,omitempty"` + Enabled bool `protobuf:"varint,9,opt,name=enabled,proto3" json:"enabled,omitempty"` + Gpgcheck bool `protobuf:"varint,10,opt,name=gpgcheck,proto3" json:"gpgcheck,omitempty"` + RepoGpgcheck bool `protobuf:"varint,11,opt,name=repo_gpgcheck,json=repoGpgcheck,proto3" json:"repo_gpgcheck,omitempty"` + GpgkeyUrls []string `protobuf:"bytes,12,rep,name=gpgkey_urls,json=gpgkeyUrls,proto3" json:"gpgkey_urls,omitempty"` + ImmutableRevision bool `protobuf:"varint,13,opt,name=immutable_revision,json=immutableRevision,proto3" json:"immutable_revision,omitempty"` +} + +func (x *RepositoryConfig) Reset() { + *x = RepositoryConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[77] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryConfig) ProtoMessage() {} + +func (x *RepositoryConfig) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[77] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryConfig.ProtoReflect.Descriptor instead. +func (*RepositoryConfig) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{77} +} + +func (x *RepositoryConfig) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *RepositoryConfig) GetActiveRevisionId() string { + if x != nil { + return x.ActiveRevisionId + } + return "" +} + +func (x *RepositoryConfig) GetRepoId() string { + if x != nil { + return x.RepoId + } + return "" +} + +func (x *RepositoryConfig) GetFileName() string { + if x != nil { + return x.FileName + } + return "" +} + +func (x *RepositoryConfig) GetContentType() string { + if x != nil { + return x.ContentType + } + return "" +} + +func (x *RepositoryConfig) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +func (x *RepositoryConfig) GetBaseurl() string { + if x != nil { + return x.Baseurl + } + return "" +} + +func (x *RepositoryConfig) GetRevisionUrl() string { + if x != nil { + return x.RevisionUrl + } + return "" +} + +func (x *RepositoryConfig) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +func (x *RepositoryConfig) GetGpgcheck() bool { + if x != nil { + return x.Gpgcheck + } + return false +} + +func (x *RepositoryConfig) GetRepoGpgcheck() bool { + if x != nil { + return x.RepoGpgcheck + } + return false +} + +func (x *RepositoryConfig) GetGpgkeyUrls() []string { + if x != nil { + return x.GpgkeyUrls + } + return nil +} + +func (x *RepositoryConfig) GetImmutableRevision() bool { + if x != nil { + return x.ImmutableRevision + } + return false +} + +type RepositoryConfigResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Config *RepositoryConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` +} + +func (x *RepositoryConfigResponse) Reset() { + *x = RepositoryConfigResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[78] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryConfigResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryConfigResponse) ProtoMessage() {} + +func (x *RepositoryConfigResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[78] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryConfigResponse.ProtoReflect.Descriptor instead. +func (*RepositoryConfigResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{78} +} + +func (x *RepositoryConfigResponse) GetConfig() *RepositoryConfig { + if x != nil { + return x.Config + } + return nil +} + +type RepositoryGPGKey struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + RepositoryId string `protobuf:"bytes,2,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + KeyId string `protobuf:"bytes,3,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty"` + Fingerprint string `protobuf:"bytes,4,opt,name=fingerprint,proto3" json:"fingerprint,omitempty"` + PublicKeyPath string `protobuf:"bytes,5,opt,name=public_key_path,json=publicKeyPath,proto3" json:"public_key_path,omitempty"` + PublicKeyUrl string `protobuf:"bytes,6,opt,name=public_key_url,json=publicKeyUrl,proto3" json:"public_key_url,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` +} + +func (x *RepositoryGPGKey) Reset() { + *x = RepositoryGPGKey{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[79] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryGPGKey) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryGPGKey) ProtoMessage() {} + +func (x *RepositoryGPGKey) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[79] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryGPGKey.ProtoReflect.Descriptor instead. +func (*RepositoryGPGKey) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{79} +} + +func (x *RepositoryGPGKey) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *RepositoryGPGKey) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *RepositoryGPGKey) GetKeyId() string { + if x != nil { + return x.KeyId + } + return "" +} + +func (x *RepositoryGPGKey) GetFingerprint() string { + if x != nil { + return x.Fingerprint + } + return "" +} + +func (x *RepositoryGPGKey) GetPublicKeyPath() string { + if x != nil { + return x.PublicKeyPath + } + return "" +} + +func (x *RepositoryGPGKey) GetPublicKeyUrl() string { + if x != nil { + return x.PublicKeyUrl + } + return "" +} + +func (x *RepositoryGPGKey) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +type RegisterRepositoryGPGKeyRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepositoryId string `protobuf:"bytes,1,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + PublicKey []byte `protobuf:"bytes,2,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` +} + +func (x *RegisterRepositoryGPGKeyRequest) Reset() { + *x = RegisterRepositoryGPGKeyRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[80] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RegisterRepositoryGPGKeyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterRepositoryGPGKeyRequest) ProtoMessage() {} + +func (x *RegisterRepositoryGPGKeyRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[80] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RegisterRepositoryGPGKeyRequest.ProtoReflect.Descriptor instead. +func (*RegisterRepositoryGPGKeyRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{80} +} + +func (x *RegisterRepositoryGPGKeyRequest) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *RegisterRepositoryGPGKeyRequest) GetPublicKey() []byte { + if x != nil { + return x.PublicKey + } + return nil +} + +type ListRepositoryGPGKeysRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepositoryId string `protobuf:"bytes,1,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` +} + +func (x *ListRepositoryGPGKeysRequest) Reset() { + *x = ListRepositoryGPGKeysRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[81] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListRepositoryGPGKeysRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRepositoryGPGKeysRequest) ProtoMessage() {} + +func (x *ListRepositoryGPGKeysRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[81] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRepositoryGPGKeysRequest.ProtoReflect.Descriptor instead. +func (*ListRepositoryGPGKeysRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{81} +} + +func (x *ListRepositoryGPGKeysRequest) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +type DeleteRepositoryGPGKeyRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepositoryId string `protobuf:"bytes,1,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + KeyId string `protobuf:"bytes,2,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty"` + ConfirmDelete bool `protobuf:"varint,3,opt,name=confirm_delete,json=confirmDelete,proto3" json:"confirm_delete,omitempty"` +} + +func (x *DeleteRepositoryGPGKeyRequest) Reset() { + *x = DeleteRepositoryGPGKeyRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[82] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteRepositoryGPGKeyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteRepositoryGPGKeyRequest) ProtoMessage() {} + +func (x *DeleteRepositoryGPGKeyRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[82] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteRepositoryGPGKeyRequest.ProtoReflect.Descriptor instead. +func (*DeleteRepositoryGPGKeyRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{82} +} + +func (x *DeleteRepositoryGPGKeyRequest) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *DeleteRepositoryGPGKeyRequest) GetKeyId() string { + if x != nil { + return x.KeyId + } + return "" +} + +func (x *DeleteRepositoryGPGKeyRequest) GetConfirmDelete() bool { + if x != nil { + return x.ConfirmDelete + } + return false +} + +type RepositoryGPGKeyResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Key *RepositoryGPGKey `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` +} + +func (x *RepositoryGPGKeyResponse) Reset() { + *x = RepositoryGPGKeyResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[83] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryGPGKeyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryGPGKeyResponse) ProtoMessage() {} + +func (x *RepositoryGPGKeyResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[83] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryGPGKeyResponse.ProtoReflect.Descriptor instead. +func (*RepositoryGPGKeyResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{83} +} + +func (x *RepositoryGPGKeyResponse) GetKey() *RepositoryGPGKey { + if x != nil { + return x.Key + } + return nil +} + +type ListRepositoryGPGKeysResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Keys []*RepositoryGPGKey `protobuf:"bytes,1,rep,name=keys,proto3" json:"keys,omitempty"` +} + +func (x *ListRepositoryGPGKeysResponse) Reset() { + *x = ListRepositoryGPGKeysResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[84] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListRepositoryGPGKeysResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRepositoryGPGKeysResponse) ProtoMessage() {} + +func (x *ListRepositoryGPGKeysResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[84] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRepositoryGPGKeysResponse.ProtoReflect.Descriptor instead. +func (*ListRepositoryGPGKeysResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{84} +} + +func (x *ListRepositoryGPGKeysResponse) GetKeys() []*RepositoryGPGKey { + if x != nil { + return x.Keys + } + return nil +} + +type ListRepositoryTasksResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Tasks []*RepositoryTask `protobuf:"bytes,1,rep,name=tasks,proto3" json:"tasks,omitempty"` + Statistics *RepositoryTaskStatistics `protobuf:"bytes,2,opt,name=statistics,proto3" json:"statistics,omitempty"` + PageResponse *common.PageResponse `protobuf:"bytes,10,opt,name=page_response,json=pageResponse,proto3" json:"page_response,omitempty"` +} + +func (x *ListRepositoryTasksResponse) Reset() { + *x = ListRepositoryTasksResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[85] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListRepositoryTasksResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRepositoryTasksResponse) ProtoMessage() {} + +func (x *ListRepositoryTasksResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[85] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRepositoryTasksResponse.ProtoReflect.Descriptor instead. +func (*ListRepositoryTasksResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{85} +} + +func (x *ListRepositoryTasksResponse) GetTasks() []*RepositoryTask { + if x != nil { + return x.Tasks + } + return nil +} + +func (x *ListRepositoryTasksResponse) GetStatistics() *RepositoryTaskStatistics { + if x != nil { + return x.Statistics + } + return nil +} + +func (x *ListRepositoryTasksResponse) GetPageResponse() *common.PageResponse { + if x != nil { + return x.PageResponse + } + return nil +} + +type RepositoryUpload struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + RepositoryId string `protobuf:"bytes,2,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + ProjectId int64 `protobuf:"varint,3,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + TaskId string `protobuf:"bytes,4,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + FileName string `protobuf:"bytes,5,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"` + Kind string `protobuf:"bytes,6,opt,name=kind,proto3" json:"kind,omitempty"` + ExpectedSize int64 `protobuf:"varint,7,opt,name=expected_size,json=expectedSize,proto3" json:"expected_size,omitempty"` + ReceivedSize int64 `protobuf:"varint,8,opt,name=received_size,json=receivedSize,proto3" json:"received_size,omitempty"` + ExpectedSha256 string `protobuf:"bytes,9,opt,name=expected_sha256,json=expectedSha256,proto3" json:"expected_sha256,omitempty"` + ActualSha256 string `protobuf:"bytes,10,opt,name=actual_sha256,json=actualSha256,proto3" json:"actual_sha256,omitempty"` + Status string `protobuf:"bytes,11,opt,name=status,proto3" json:"status,omitempty"` + ErrorCode string `protobuf:"bytes,12,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"` + ErrorMessage string `protobuf:"bytes,13,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` + ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,14,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` + CompletedAt *timestamppb.Timestamp `protobuf:"bytes,15,opt,name=completed_at,json=completedAt,proto3" json:"completed_at,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,16,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,17,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` + StorageNodeId string `protobuf:"bytes,18,opt,name=storage_node_id,json=storageNodeId,proto3" json:"storage_node_id,omitempty"` + ClientFingerprint string `protobuf:"bytes,19,opt,name=client_fingerprint,json=clientFingerprint,proto3" json:"client_fingerprint,omitempty"` + ChunkSize int32 `protobuf:"varint,20,opt,name=chunk_size,json=chunkSize,proto3" json:"chunk_size,omitempty"` + TotalChunks int32 `protobuf:"varint,21,opt,name=total_chunks,json=totalChunks,proto3" json:"total_chunks,omitempty"` + UploadedCount int32 `protobuf:"varint,22,opt,name=uploaded_count,json=uploadedCount,proto3" json:"uploaded_count,omitempty"` + PausedAt *timestamppb.Timestamp `protobuf:"bytes,23,opt,name=paused_at,json=pausedAt,proto3" json:"paused_at,omitempty"` +} + +func (x *RepositoryUpload) Reset() { + *x = RepositoryUpload{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[86] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryUpload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryUpload) ProtoMessage() {} + +func (x *RepositoryUpload) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[86] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryUpload.ProtoReflect.Descriptor instead. +func (*RepositoryUpload) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{86} +} + +func (x *RepositoryUpload) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *RepositoryUpload) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *RepositoryUpload) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +func (x *RepositoryUpload) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *RepositoryUpload) GetFileName() string { + if x != nil { + return x.FileName + } + return "" +} + +func (x *RepositoryUpload) GetKind() string { + if x != nil { + return x.Kind + } + return "" +} + +func (x *RepositoryUpload) GetExpectedSize() int64 { + if x != nil { + return x.ExpectedSize + } + return 0 +} + +func (x *RepositoryUpload) GetReceivedSize() int64 { + if x != nil { + return x.ReceivedSize + } + return 0 +} + +func (x *RepositoryUpload) GetExpectedSha256() string { + if x != nil { + return x.ExpectedSha256 + } + return "" +} + +func (x *RepositoryUpload) GetActualSha256() string { + if x != nil { + return x.ActualSha256 + } + return "" +} + +func (x *RepositoryUpload) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *RepositoryUpload) GetErrorCode() string { + if x != nil { + return x.ErrorCode + } + return "" +} + +func (x *RepositoryUpload) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +func (x *RepositoryUpload) GetExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.ExpiresAt + } + return nil +} + +func (x *RepositoryUpload) GetCompletedAt() *timestamppb.Timestamp { + if x != nil { + return x.CompletedAt + } + return nil +} + +func (x *RepositoryUpload) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *RepositoryUpload) GetUpdatedAt() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedAt + } + return nil +} + +func (x *RepositoryUpload) GetStorageNodeId() string { + if x != nil { + return x.StorageNodeId + } + return "" +} + +func (x *RepositoryUpload) GetClientFingerprint() string { + if x != nil { + return x.ClientFingerprint + } + return "" +} + +func (x *RepositoryUpload) GetChunkSize() int32 { + if x != nil { + return x.ChunkSize + } + return 0 +} + +func (x *RepositoryUpload) GetTotalChunks() int32 { + if x != nil { + return x.TotalChunks + } + return 0 +} + +func (x *RepositoryUpload) GetUploadedCount() int32 { + if x != nil { + return x.UploadedCount + } + return 0 +} + +func (x *RepositoryUpload) GetPausedAt() *timestamppb.Timestamp { + if x != nil { + return x.PausedAt + } + return nil +} + +type BeginRepositoryUploadRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepositoryId string `protobuf:"bytes,1,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + FileName string `protobuf:"bytes,2,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"` + TotalSize int64 `protobuf:"varint,3,opt,name=total_size,json=totalSize,proto3" json:"total_size,omitempty"` + ExpectedSha256 string `protobuf:"bytes,4,opt,name=expected_sha256,json=expectedSha256,proto3" json:"expected_sha256,omitempty"` + ClientFingerprint string `protobuf:"bytes,5,opt,name=client_fingerprint,json=clientFingerprint,proto3" json:"client_fingerprint,omitempty"` + ChunkSize int32 `protobuf:"varint,6,opt,name=chunk_size,json=chunkSize,proto3" json:"chunk_size,omitempty"` +} + +func (x *BeginRepositoryUploadRequest) Reset() { + *x = BeginRepositoryUploadRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[87] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *BeginRepositoryUploadRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BeginRepositoryUploadRequest) ProtoMessage() {} + +func (x *BeginRepositoryUploadRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[87] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BeginRepositoryUploadRequest.ProtoReflect.Descriptor instead. +func (*BeginRepositoryUploadRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{87} +} + +func (x *BeginRepositoryUploadRequest) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *BeginRepositoryUploadRequest) GetFileName() string { + if x != nil { + return x.FileName + } + return "" +} + +func (x *BeginRepositoryUploadRequest) GetTotalSize() int64 { + if x != nil { + return x.TotalSize + } + return 0 +} + +func (x *BeginRepositoryUploadRequest) GetExpectedSha256() string { + if x != nil { + return x.ExpectedSha256 + } + return "" +} + +func (x *BeginRepositoryUploadRequest) GetClientFingerprint() string { + if x != nil { + return x.ClientFingerprint + } + return "" +} + +func (x *BeginRepositoryUploadRequest) GetChunkSize() int32 { + if x != nil { + return x.ChunkSize + } + return 0 +} + +type AppendRepositoryUploadChunkRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + UploadId string `protobuf:"bytes,1,opt,name=upload_id,json=uploadId,proto3" json:"upload_id,omitempty"` + Offset int64 `protobuf:"varint,2,opt,name=offset,proto3" json:"offset,omitempty"` + Data []byte `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` +} + +func (x *AppendRepositoryUploadChunkRequest) Reset() { + *x = AppendRepositoryUploadChunkRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[88] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AppendRepositoryUploadChunkRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AppendRepositoryUploadChunkRequest) ProtoMessage() {} + +func (x *AppendRepositoryUploadChunkRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[88] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AppendRepositoryUploadChunkRequest.ProtoReflect.Descriptor instead. +func (*AppendRepositoryUploadChunkRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{88} +} + +func (x *AppendRepositoryUploadChunkRequest) GetUploadId() string { + if x != nil { + return x.UploadId + } + return "" +} + +func (x *AppendRepositoryUploadChunkRequest) GetOffset() int64 { + if x != nil { + return x.Offset + } + return 0 +} + +func (x *AppendRepositoryUploadChunkRequest) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +type PutRepositoryUploadChunkRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + UploadId string `protobuf:"bytes,1,opt,name=upload_id,json=uploadId,proto3" json:"upload_id,omitempty"` + ChunkIndex int32 `protobuf:"varint,2,opt,name=chunk_index,json=chunkIndex,proto3" json:"chunk_index,omitempty"` + ChunkSha256 string `protobuf:"bytes,3,opt,name=chunk_sha256,json=chunkSha256,proto3" json:"chunk_sha256,omitempty"` + Data []byte `protobuf:"bytes,4,opt,name=data,proto3" json:"data,omitempty"` +} + +func (x *PutRepositoryUploadChunkRequest) Reset() { + *x = PutRepositoryUploadChunkRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[89] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PutRepositoryUploadChunkRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PutRepositoryUploadChunkRequest) ProtoMessage() {} + +func (x *PutRepositoryUploadChunkRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[89] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PutRepositoryUploadChunkRequest.ProtoReflect.Descriptor instead. +func (*PutRepositoryUploadChunkRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{89} +} + +func (x *PutRepositoryUploadChunkRequest) GetUploadId() string { + if x != nil { + return x.UploadId + } + return "" +} + +func (x *PutRepositoryUploadChunkRequest) GetChunkIndex() int32 { + if x != nil { + return x.ChunkIndex + } + return 0 +} + +func (x *PutRepositoryUploadChunkRequest) GetChunkSha256() string { + if x != nil { + return x.ChunkSha256 + } + return "" +} + +func (x *PutRepositoryUploadChunkRequest) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +type RepositoryUploadChunk struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ChunkIndex int32 `protobuf:"varint,1,opt,name=chunk_index,json=chunkIndex,proto3" json:"chunk_index,omitempty"` + SizeBytes int32 `protobuf:"varint,2,opt,name=size_bytes,json=sizeBytes,proto3" json:"size_bytes,omitempty"` + Sha256 string `protobuf:"bytes,3,opt,name=sha256,proto3" json:"sha256,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` +} + +func (x *RepositoryUploadChunk) Reset() { + *x = RepositoryUploadChunk{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[90] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryUploadChunk) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryUploadChunk) ProtoMessage() {} + +func (x *RepositoryUploadChunk) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[90] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryUploadChunk.ProtoReflect.Descriptor instead. +func (*RepositoryUploadChunk) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{90} +} + +func (x *RepositoryUploadChunk) GetChunkIndex() int32 { + if x != nil { + return x.ChunkIndex + } + return 0 +} + +func (x *RepositoryUploadChunk) GetSizeBytes() int32 { + if x != nil { + return x.SizeBytes + } + return 0 +} + +func (x *RepositoryUploadChunk) GetSha256() string { + if x != nil { + return x.Sha256 + } + return "" +} + +func (x *RepositoryUploadChunk) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +type PutRepositoryUploadChunkResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Upload *RepositoryUpload `protobuf:"bytes,1,opt,name=upload,proto3" json:"upload,omitempty"` + Chunk *RepositoryUploadChunk `protobuf:"bytes,2,opt,name=chunk,proto3" json:"chunk,omitempty"` +} + +func (x *PutRepositoryUploadChunkResponse) Reset() { + *x = PutRepositoryUploadChunkResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[91] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PutRepositoryUploadChunkResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PutRepositoryUploadChunkResponse) ProtoMessage() {} + +func (x *PutRepositoryUploadChunkResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[91] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PutRepositoryUploadChunkResponse.ProtoReflect.Descriptor instead. +func (*PutRepositoryUploadChunkResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{91} +} + +func (x *PutRepositoryUploadChunkResponse) GetUpload() *RepositoryUpload { + if x != nil { + return x.Upload + } + return nil +} + +func (x *PutRepositoryUploadChunkResponse) GetChunk() *RepositoryUploadChunk { + if x != nil { + return x.Chunk + } + return nil +} + +type DescribeRepositoryUploadChunksResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Upload *RepositoryUpload `protobuf:"bytes,1,opt,name=upload,proto3" json:"upload,omitempty"` + Chunks []*RepositoryUploadChunk `protobuf:"bytes,2,rep,name=chunks,proto3" json:"chunks,omitempty"` + MissingChunkIndexes []int32 `protobuf:"varint,3,rep,packed,name=missing_chunk_indexes,json=missingChunkIndexes,proto3" json:"missing_chunk_indexes,omitempty"` +} + +func (x *DescribeRepositoryUploadChunksResponse) Reset() { + *x = DescribeRepositoryUploadChunksResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[92] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DescribeRepositoryUploadChunksResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeRepositoryUploadChunksResponse) ProtoMessage() {} + +func (x *DescribeRepositoryUploadChunksResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[92] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeRepositoryUploadChunksResponse.ProtoReflect.Descriptor instead. +func (*DescribeRepositoryUploadChunksResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{92} +} + +func (x *DescribeRepositoryUploadChunksResponse) GetUpload() *RepositoryUpload { + if x != nil { + return x.Upload + } + return nil +} + +func (x *DescribeRepositoryUploadChunksResponse) GetChunks() []*RepositoryUploadChunk { + if x != nil { + return x.Chunks + } + return nil +} + +func (x *DescribeRepositoryUploadChunksResponse) GetMissingChunkIndexes() []int32 { + if x != nil { + return x.MissingChunkIndexes + } + return nil +} + +type CompleteRepositoryUploadRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + UploadId string `protobuf:"bytes,1,opt,name=upload_id,json=uploadId,proto3" json:"upload_id,omitempty"` +} + +func (x *CompleteRepositoryUploadRequest) Reset() { + *x = CompleteRepositoryUploadRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[93] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CompleteRepositoryUploadRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CompleteRepositoryUploadRequest) ProtoMessage() {} + +func (x *CompleteRepositoryUploadRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[93] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CompleteRepositoryUploadRequest.ProtoReflect.Descriptor instead. +func (*CompleteRepositoryUploadRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{93} +} + +func (x *CompleteRepositoryUploadRequest) GetUploadId() string { + if x != nil { + return x.UploadId + } + return "" +} + +type AbortRepositoryUploadRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + UploadId string `protobuf:"bytes,1,opt,name=upload_id,json=uploadId,proto3" json:"upload_id,omitempty"` +} + +func (x *AbortRepositoryUploadRequest) Reset() { + *x = AbortRepositoryUploadRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[94] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AbortRepositoryUploadRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AbortRepositoryUploadRequest) ProtoMessage() {} + +func (x *AbortRepositoryUploadRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[94] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AbortRepositoryUploadRequest.ProtoReflect.Descriptor instead. +func (*AbortRepositoryUploadRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{94} +} + +func (x *AbortRepositoryUploadRequest) GetUploadId() string { + if x != nil { + return x.UploadId + } + return "" +} + +type DescribeRepositoryUploadRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + UploadId string `protobuf:"bytes,1,opt,name=upload_id,json=uploadId,proto3" json:"upload_id,omitempty"` +} + +func (x *DescribeRepositoryUploadRequest) Reset() { + *x = DescribeRepositoryUploadRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[95] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DescribeRepositoryUploadRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeRepositoryUploadRequest) ProtoMessage() {} + +func (x *DescribeRepositoryUploadRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[95] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeRepositoryUploadRequest.ProtoReflect.Descriptor instead. +func (*DescribeRepositoryUploadRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{95} +} + +func (x *DescribeRepositoryUploadRequest) GetUploadId() string { + if x != nil { + return x.UploadId + } + return "" +} + +type RepositoryUploadResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Upload *RepositoryUpload `protobuf:"bytes,1,opt,name=upload,proto3" json:"upload,omitempty"` +} + +func (x *RepositoryUploadResponse) Reset() { + *x = RepositoryUploadResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[96] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryUploadResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryUploadResponse) ProtoMessage() {} + +func (x *RepositoryUploadResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[96] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryUploadResponse.ProtoReflect.Descriptor instead. +func (*RepositoryUploadResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{96} +} + +func (x *RepositoryUploadResponse) GetUpload() *RepositoryUpload { + if x != nil { + return x.Upload + } + return nil +} + +type CompleteRepositoryUploadResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Upload *RepositoryUpload `protobuf:"bytes,1,opt,name=upload,proto3" json:"upload,omitempty"` + Task *RepositoryTask `protobuf:"bytes,2,opt,name=task,proto3" json:"task,omitempty"` +} + +func (x *CompleteRepositoryUploadResponse) Reset() { + *x = CompleteRepositoryUploadResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[97] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CompleteRepositoryUploadResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CompleteRepositoryUploadResponse) ProtoMessage() {} + +func (x *CompleteRepositoryUploadResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[97] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CompleteRepositoryUploadResponse.ProtoReflect.Descriptor instead. +func (*CompleteRepositoryUploadResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{97} +} + +func (x *CompleteRepositoryUploadResponse) GetUpload() *RepositoryUpload { + if x != nil { + return x.Upload + } + return nil +} + +func (x *CompleteRepositoryUploadResponse) GetTask() *RepositoryTask { + if x != nil { + return x.Task + } + return nil +} + +type RepositoryPackage struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + RepositoryId string `protobuf:"bytes,2,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + RevisionId string `protobuf:"bytes,3,opt,name=revision_id,json=revisionId,proto3" json:"revision_id,omitempty"` + Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` + Epoch string `protobuf:"bytes,5,opt,name=epoch,proto3" json:"epoch,omitempty"` + Version string `protobuf:"bytes,6,opt,name=version,proto3" json:"version,omitempty"` + Release string `protobuf:"bytes,7,opt,name=release,proto3" json:"release,omitempty"` + Architecture string `protobuf:"bytes,8,opt,name=architecture,proto3" json:"architecture,omitempty"` + Nevra string `protobuf:"bytes,9,opt,name=nevra,proto3" json:"nevra,omitempty"` + FileName string `protobuf:"bytes,10,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"` + SizeBytes int64 `protobuf:"varint,11,opt,name=size_bytes,json=sizeBytes,proto3" json:"size_bytes,omitempty"` + Sha256 string `protobuf:"bytes,12,opt,name=sha256,proto3" json:"sha256,omitempty"` + Source string `protobuf:"bytes,13,opt,name=source,proto3" json:"source,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,14,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + SignatureStatus string `protobuf:"bytes,15,opt,name=signature_status,json=signatureStatus,proto3" json:"signature_status,omitempty"` + SignerKeyId string `protobuf:"bytes,16,opt,name=signer_key_id,json=signerKeyId,proto3" json:"signer_key_id,omitempty"` + SignatureMessage string `protobuf:"bytes,17,opt,name=signature_message,json=signatureMessage,proto3" json:"signature_message,omitempty"` +} + +func (x *RepositoryPackage) Reset() { + *x = RepositoryPackage{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[98] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryPackage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryPackage) ProtoMessage() {} + +func (x *RepositoryPackage) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[98] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryPackage.ProtoReflect.Descriptor instead. +func (*RepositoryPackage) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{98} +} + +func (x *RepositoryPackage) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *RepositoryPackage) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *RepositoryPackage) GetRevisionId() string { + if x != nil { + return x.RevisionId + } + return "" +} + +func (x *RepositoryPackage) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *RepositoryPackage) GetEpoch() string { + if x != nil { + return x.Epoch + } + return "" +} + +func (x *RepositoryPackage) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *RepositoryPackage) GetRelease() string { + if x != nil { + return x.Release + } + return "" +} + +func (x *RepositoryPackage) GetArchitecture() string { + if x != nil { + return x.Architecture + } + return "" +} + +func (x *RepositoryPackage) GetNevra() string { + if x != nil { + return x.Nevra + } + return "" +} + +func (x *RepositoryPackage) GetFileName() string { + if x != nil { + return x.FileName + } + return "" +} + +func (x *RepositoryPackage) GetSizeBytes() int64 { + if x != nil { + return x.SizeBytes + } + return 0 +} + +func (x *RepositoryPackage) GetSha256() string { + if x != nil { + return x.Sha256 + } + return "" +} + +func (x *RepositoryPackage) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +func (x *RepositoryPackage) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *RepositoryPackage) GetSignatureStatus() string { + if x != nil { + return x.SignatureStatus + } + return "" +} + +func (x *RepositoryPackage) GetSignerKeyId() string { + if x != nil { + return x.SignerKeyId + } + return "" +} + +func (x *RepositoryPackage) GetSignatureMessage() string { + if x != nil { + return x.SignatureMessage + } + return "" +} + +type ListRepositoryPackagesRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepositoryId string `protobuf:"bytes,1,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + RevisionId string `protobuf:"bytes,2,opt,name=revision_id,json=revisionId,proto3" json:"revision_id,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + Architecture RepositoryArchitecture `protobuf:"varint,4,opt,name=architecture,proto3,enum=trpc.ocmanager.repository.RepositoryArchitecture" json:"architecture,omitempty"` + PageRequest *common.PageRequest `protobuf:"bytes,10,opt,name=page_request,json=pageRequest,proto3" json:"page_request,omitempty"` +} + +func (x *ListRepositoryPackagesRequest) Reset() { + *x = ListRepositoryPackagesRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[99] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListRepositoryPackagesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRepositoryPackagesRequest) ProtoMessage() {} + +func (x *ListRepositoryPackagesRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[99] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRepositoryPackagesRequest.ProtoReflect.Descriptor instead. +func (*ListRepositoryPackagesRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{99} +} + +func (x *ListRepositoryPackagesRequest) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *ListRepositoryPackagesRequest) GetRevisionId() string { + if x != nil { + return x.RevisionId + } + return "" +} + +func (x *ListRepositoryPackagesRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ListRepositoryPackagesRequest) GetArchitecture() RepositoryArchitecture { + if x != nil { + return x.Architecture + } + return RepositoryArchitecture_REPOSITORY_ARCHITECTURE_UNSPECIFIED +} + +func (x *ListRepositoryPackagesRequest) GetPageRequest() *common.PageRequest { + if x != nil { + return x.PageRequest + } + return nil +} + +type ListRepositoryPackagesResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RevisionId string `protobuf:"bytes,1,opt,name=revision_id,json=revisionId,proto3" json:"revision_id,omitempty"` + Packages []*RepositoryPackage `protobuf:"bytes,2,rep,name=packages,proto3" json:"packages,omitempty"` + PageResponse *common.PageResponse `protobuf:"bytes,10,opt,name=page_response,json=pageResponse,proto3" json:"page_response,omitempty"` +} + +func (x *ListRepositoryPackagesResponse) Reset() { + *x = ListRepositoryPackagesResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[100] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListRepositoryPackagesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRepositoryPackagesResponse) ProtoMessage() {} + +func (x *ListRepositoryPackagesResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[100] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRepositoryPackagesResponse.ProtoReflect.Descriptor instead. +func (*ListRepositoryPackagesResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{100} +} + +func (x *ListRepositoryPackagesResponse) GetRevisionId() string { + if x != nil { + return x.RevisionId + } + return "" +} + +func (x *ListRepositoryPackagesResponse) GetPackages() []*RepositoryPackage { + if x != nil { + return x.Packages + } + return nil +} + +func (x *ListRepositoryPackagesResponse) GetPageResponse() *common.PageResponse { + if x != nil { + return x.PageResponse + } + return nil +} + +type RepositoryQuarantineItem struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + RepositoryId string `protobuf:"bytes,2,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + TaskId string `protobuf:"bytes,3,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + UploadId string `protobuf:"bytes,4,opt,name=upload_id,json=uploadId,proto3" json:"upload_id,omitempty"` + OriginalName string `protobuf:"bytes,5,opt,name=original_name,json=originalName,proto3" json:"original_name,omitempty"` + SizeBytes int64 `protobuf:"varint,6,opt,name=size_bytes,json=sizeBytes,proto3" json:"size_bytes,omitempty"` + Sha256 string `protobuf:"bytes,7,opt,name=sha256,proto3" json:"sha256,omitempty"` + ReasonCode string `protobuf:"bytes,8,opt,name=reason_code,json=reasonCode,proto3" json:"reason_code,omitempty"` + ReasonMessage string `protobuf:"bytes,9,opt,name=reason_message,json=reasonMessage,proto3" json:"reason_message,omitempty"` + DetectedNevra string `protobuf:"bytes,10,opt,name=detected_nevra,json=detectedNevra,proto3" json:"detected_nevra,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` +} + +func (x *RepositoryQuarantineItem) Reset() { + *x = RepositoryQuarantineItem{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[101] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryQuarantineItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryQuarantineItem) ProtoMessage() {} + +func (x *RepositoryQuarantineItem) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[101] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryQuarantineItem.ProtoReflect.Descriptor instead. +func (*RepositoryQuarantineItem) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{101} +} + +func (x *RepositoryQuarantineItem) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *RepositoryQuarantineItem) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *RepositoryQuarantineItem) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *RepositoryQuarantineItem) GetUploadId() string { + if x != nil { + return x.UploadId + } + return "" +} + +func (x *RepositoryQuarantineItem) GetOriginalName() string { + if x != nil { + return x.OriginalName + } + return "" +} + +func (x *RepositoryQuarantineItem) GetSizeBytes() int64 { + if x != nil { + return x.SizeBytes + } + return 0 +} + +func (x *RepositoryQuarantineItem) GetSha256() string { + if x != nil { + return x.Sha256 + } + return "" +} + +func (x *RepositoryQuarantineItem) GetReasonCode() string { + if x != nil { + return x.ReasonCode + } + return "" +} + +func (x *RepositoryQuarantineItem) GetReasonMessage() string { + if x != nil { + return x.ReasonMessage + } + return "" +} + +func (x *RepositoryQuarantineItem) GetDetectedNevra() string { + if x != nil { + return x.DetectedNevra + } + return "" +} + +func (x *RepositoryQuarantineItem) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +type ListRepositoryQuarantineRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepositoryId string `protobuf:"bytes,1,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + TaskId string `protobuf:"bytes,2,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + ReasonCode string `protobuf:"bytes,3,opt,name=reason_code,json=reasonCode,proto3" json:"reason_code,omitempty"` + PageRequest *common.PageRequest `protobuf:"bytes,10,opt,name=page_request,json=pageRequest,proto3" json:"page_request,omitempty"` +} + +func (x *ListRepositoryQuarantineRequest) Reset() { + *x = ListRepositoryQuarantineRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[102] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListRepositoryQuarantineRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRepositoryQuarantineRequest) ProtoMessage() {} + +func (x *ListRepositoryQuarantineRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[102] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRepositoryQuarantineRequest.ProtoReflect.Descriptor instead. +func (*ListRepositoryQuarantineRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{102} +} + +func (x *ListRepositoryQuarantineRequest) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *ListRepositoryQuarantineRequest) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *ListRepositoryQuarantineRequest) GetReasonCode() string { + if x != nil { + return x.ReasonCode + } + return "" +} + +func (x *ListRepositoryQuarantineRequest) GetPageRequest() *common.PageRequest { + if x != nil { + return x.PageRequest + } + return nil +} + +type ListRepositoryQuarantineResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Items []*RepositoryQuarantineItem `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + PageResponse *common.PageResponse `protobuf:"bytes,10,opt,name=page_response,json=pageResponse,proto3" json:"page_response,omitempty"` +} + +func (x *ListRepositoryQuarantineResponse) Reset() { + *x = ListRepositoryQuarantineResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[103] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListRepositoryQuarantineResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRepositoryQuarantineResponse) ProtoMessage() {} + +func (x *ListRepositoryQuarantineResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[103] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRepositoryQuarantineResponse.ProtoReflect.Descriptor instead. +func (*ListRepositoryQuarantineResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{103} +} + +func (x *ListRepositoryQuarantineResponse) GetItems() []*RepositoryQuarantineItem { + if x != nil { + return x.Items + } + return nil +} + +func (x *ListRepositoryQuarantineResponse) GetPageResponse() *common.PageResponse { + if x != nil { + return x.PageResponse + } + return nil +} + +type DeleteRepositoryPackageRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepositoryId string `protobuf:"bytes,1,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + Nevra string `protobuf:"bytes,2,opt,name=nevra,proto3" json:"nevra,omitempty"` +} + +func (x *DeleteRepositoryPackageRequest) Reset() { + *x = DeleteRepositoryPackageRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[104] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteRepositoryPackageRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteRepositoryPackageRequest) ProtoMessage() {} + +func (x *DeleteRepositoryPackageRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[104] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteRepositoryPackageRequest.ProtoReflect.Descriptor instead. +func (*DeleteRepositoryPackageRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{104} +} + +func (x *DeleteRepositoryPackageRequest) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *DeleteRepositoryPackageRequest) GetNevra() string { + if x != nil { + return x.Nevra + } + return "" +} + +type RepositoryDiagnosis struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + FailureCategory string `protobuf:"bytes,1,opt,name=failure_category,json=failureCategory,proto3" json:"failure_category,omitempty"` + ErrorCode string `protobuf:"bytes,2,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"` + FailedStep string `protobuf:"bytes,3,opt,name=failed_step,json=failedStep,proto3" json:"failed_step,omitempty"` + LikelyCause string `protobuf:"bytes,4,opt,name=likely_cause,json=likelyCause,proto3" json:"likely_cause,omitempty"` + Evidence []string `protobuf:"bytes,5,rep,name=evidence,proto3" json:"evidence,omitempty"` + RecommendedAction string `protobuf:"bytes,6,opt,name=recommended_action,json=recommendedAction,proto3" json:"recommended_action,omitempty"` + RuleId string `protobuf:"bytes,7,opt,name=rule_id,json=ruleId,proto3" json:"rule_id,omitempty"` + GeneratedAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=generated_at,json=generatedAt,proto3" json:"generated_at,omitempty"` +} + +func (x *RepositoryDiagnosis) Reset() { + *x = RepositoryDiagnosis{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[105] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryDiagnosis) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryDiagnosis) ProtoMessage() {} + +func (x *RepositoryDiagnosis) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[105] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryDiagnosis.ProtoReflect.Descriptor instead. +func (*RepositoryDiagnosis) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{105} +} + +func (x *RepositoryDiagnosis) GetFailureCategory() string { + if x != nil { + return x.FailureCategory + } + return "" +} + +func (x *RepositoryDiagnosis) GetErrorCode() string { + if x != nil { + return x.ErrorCode + } + return "" +} + +func (x *RepositoryDiagnosis) GetFailedStep() string { + if x != nil { + return x.FailedStep + } + return "" +} + +func (x *RepositoryDiagnosis) GetLikelyCause() string { + if x != nil { + return x.LikelyCause + } + return "" +} + +func (x *RepositoryDiagnosis) GetEvidence() []string { + if x != nil { + return x.Evidence + } + return nil +} + +func (x *RepositoryDiagnosis) GetRecommendedAction() string { + if x != nil { + return x.RecommendedAction + } + return "" +} + +func (x *RepositoryDiagnosis) GetRuleId() string { + if x != nil { + return x.RuleId + } + return "" +} + +func (x *RepositoryDiagnosis) GetGeneratedAt() *timestamppb.Timestamp { + if x != nil { + return x.GeneratedAt + } + return nil +} + +type ExportRepositoryTaskDiagnosisRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` +} + +func (x *ExportRepositoryTaskDiagnosisRequest) Reset() { + *x = ExportRepositoryTaskDiagnosisRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[106] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ExportRepositoryTaskDiagnosisRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExportRepositoryTaskDiagnosisRequest) ProtoMessage() {} + +func (x *ExportRepositoryTaskDiagnosisRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[106] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExportRepositoryTaskDiagnosisRequest.ProtoReflect.Descriptor instead. +func (*ExportRepositoryTaskDiagnosisRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{106} +} + +func (x *ExportRepositoryTaskDiagnosisRequest) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +type RepositoryDiagnosisReportResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + FileName string `protobuf:"bytes,1,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"` + ContentType string `protobuf:"bytes,2,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"` + Sha256 string `protobuf:"bytes,3,opt,name=sha256,proto3" json:"sha256,omitempty"` + Content string `protobuf:"bytes,4,opt,name=content,proto3" json:"content,omitempty"` +} + +func (x *RepositoryDiagnosisReportResponse) Reset() { + *x = RepositoryDiagnosisReportResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[107] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryDiagnosisReportResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryDiagnosisReportResponse) ProtoMessage() {} + +func (x *RepositoryDiagnosisReportResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[107] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryDiagnosisReportResponse.ProtoReflect.Descriptor instead. +func (*RepositoryDiagnosisReportResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{107} +} + +func (x *RepositoryDiagnosisReportResponse) GetFileName() string { + if x != nil { + return x.FileName + } + return "" +} + +func (x *RepositoryDiagnosisReportResponse) GetContentType() string { + if x != nil { + return x.ContentType + } + return "" +} + +func (x *RepositoryDiagnosisReportResponse) GetSha256() string { + if x != nil { + return x.Sha256 + } + return "" +} + +func (x *RepositoryDiagnosisReportResponse) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +type RepositoryDistributionNode struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + AgentId string `protobuf:"bytes,1,opt,name=agent_id,json=agentId,proto3" json:"agent_id,omitempty"` + Ip string `protobuf:"bytes,2,opt,name=ip,proto3" json:"ip,omitempty"` + AssetStatus string `protobuf:"bytes,3,opt,name=asset_status,json=assetStatus,proto3" json:"asset_status,omitempty"` + Connected bool `protobuf:"varint,4,opt,name=connected,proto3" json:"connected,omitempty"` + LastHeartbeatAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=last_heartbeat_at,json=lastHeartbeatAt,proto3" json:"last_heartbeat_at,omitempty"` +} + +func (x *RepositoryDistributionNode) Reset() { + *x = RepositoryDistributionNode{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[108] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryDistributionNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryDistributionNode) ProtoMessage() {} + +func (x *RepositoryDistributionNode) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[108] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryDistributionNode.ProtoReflect.Descriptor instead. +func (*RepositoryDistributionNode) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{108} +} + +func (x *RepositoryDistributionNode) GetAgentId() string { + if x != nil { + return x.AgentId + } + return "" +} + +func (x *RepositoryDistributionNode) GetIp() string { + if x != nil { + return x.Ip + } + return "" +} + +func (x *RepositoryDistributionNode) GetAssetStatus() string { + if x != nil { + return x.AssetStatus + } + return "" +} + +func (x *RepositoryDistributionNode) GetConnected() bool { + if x != nil { + return x.Connected + } + return false +} + +func (x *RepositoryDistributionNode) GetLastHeartbeatAt() *timestamppb.Timestamp { + if x != nil { + return x.LastHeartbeatAt + } + return nil +} + +type ListRepositoryDistributionNodesRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *ListRepositoryDistributionNodesRequest) Reset() { + *x = ListRepositoryDistributionNodesRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[109] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListRepositoryDistributionNodesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRepositoryDistributionNodesRequest) ProtoMessage() {} + +func (x *ListRepositoryDistributionNodesRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[109] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRepositoryDistributionNodesRequest.ProtoReflect.Descriptor instead. +func (*ListRepositoryDistributionNodesRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{109} +} + +type ListRepositoryDistributionNodesResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Nodes []*RepositoryDistributionNode `protobuf:"bytes,1,rep,name=nodes,proto3" json:"nodes,omitempty"` +} + +func (x *ListRepositoryDistributionNodesResponse) Reset() { + *x = ListRepositoryDistributionNodesResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[110] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListRepositoryDistributionNodesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRepositoryDistributionNodesResponse) ProtoMessage() {} + +func (x *ListRepositoryDistributionNodesResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[110] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRepositoryDistributionNodesResponse.ProtoReflect.Descriptor instead. +func (*ListRepositoryDistributionNodesResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{110} +} + +func (x *ListRepositoryDistributionNodesResponse) GetNodes() []*RepositoryDistributionNode { + if x != nil { + return x.Nodes + } + return nil +} + +type CreateRepositoryDistributionRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepositoryId string `protobuf:"bytes,1,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + RevisionId string `protobuf:"bytes,2,opt,name=revision_id,json=revisionId,proto3" json:"revision_id,omitempty"` + TargetIps []string `protobuf:"bytes,3,rep,name=target_ips,json=targetIps,proto3" json:"target_ips,omitempty"` + TimeoutSeconds int32 `protobuf:"varint,4,opt,name=timeout_seconds,json=timeoutSeconds,proto3" json:"timeout_seconds,omitempty"` +} + +func (x *CreateRepositoryDistributionRequest) Reset() { + *x = CreateRepositoryDistributionRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[111] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateRepositoryDistributionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateRepositoryDistributionRequest) ProtoMessage() {} + +func (x *CreateRepositoryDistributionRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[111] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateRepositoryDistributionRequest.ProtoReflect.Descriptor instead. +func (*CreateRepositoryDistributionRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{111} +} + +func (x *CreateRepositoryDistributionRequest) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *CreateRepositoryDistributionRequest) GetRevisionId() string { + if x != nil { + return x.RevisionId + } + return "" +} + +func (x *CreateRepositoryDistributionRequest) GetTargetIps() []string { + if x != nil { + return x.TargetIps + } + return nil +} + +func (x *CreateRepositoryDistributionRequest) GetTimeoutSeconds() int32 { + if x != nil { + return x.TimeoutSeconds + } + return 0 +} + +type DescribeRepositoryDistributionRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + DistributionId string `protobuf:"bytes,1,opt,name=distribution_id,json=distributionId,proto3" json:"distribution_id,omitempty"` +} + +func (x *DescribeRepositoryDistributionRequest) Reset() { + *x = DescribeRepositoryDistributionRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[112] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DescribeRepositoryDistributionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeRepositoryDistributionRequest) ProtoMessage() {} + +func (x *DescribeRepositoryDistributionRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[112] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeRepositoryDistributionRequest.ProtoReflect.Descriptor instead. +func (*DescribeRepositoryDistributionRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{112} +} + +func (x *DescribeRepositoryDistributionRequest) GetDistributionId() string { + if x != nil { + return x.DistributionId + } + return "" +} + +type ListRepositoryDistributionsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepositoryId string `protobuf:"bytes,1,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + PageRequest *common.PageRequest `protobuf:"bytes,10,opt,name=page_request,json=pageRequest,proto3" json:"page_request,omitempty"` +} + +func (x *ListRepositoryDistributionsRequest) Reset() { + *x = ListRepositoryDistributionsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[113] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListRepositoryDistributionsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRepositoryDistributionsRequest) ProtoMessage() {} + +func (x *ListRepositoryDistributionsRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[113] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRepositoryDistributionsRequest.ProtoReflect.Descriptor instead. +func (*ListRepositoryDistributionsRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{113} +} + +func (x *ListRepositoryDistributionsRequest) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *ListRepositoryDistributionsRequest) GetPageRequest() *common.PageRequest { + if x != nil { + return x.PageRequest + } + return nil +} + +type RepositoryDistributionTarget struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + TargetIp string `protobuf:"bytes,2,opt,name=target_ip,json=targetIp,proto3" json:"target_ip,omitempty"` + AgentId string `protobuf:"bytes,3,opt,name=agent_id,json=agentId,proto3" json:"agent_id,omitempty"` + Status string `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"` + AttemptCount int32 `protobuf:"varint,5,opt,name=attempt_count,json=attemptCount,proto3" json:"attempt_count,omitempty"` + ExitCode int32 `protobuf:"varint,6,opt,name=exit_code,json=exitCode,proto3" json:"exit_code,omitempty"` + Output string `protobuf:"bytes,7,opt,name=output,proto3" json:"output,omitempty"` + OutputRef string `protobuf:"bytes,8,opt,name=output_ref,json=outputRef,proto3" json:"output_ref,omitempty"` + OutputTruncated bool `protobuf:"varint,9,opt,name=output_truncated,json=outputTruncated,proto3" json:"output_truncated,omitempty"` + ErrorCode string `protobuf:"bytes,10,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"` + ErrorMessage string `protobuf:"bytes,11,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` + StartedAt *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=started_at,json=startedAt,proto3" json:"started_at,omitempty"` + FinishedAt *timestamppb.Timestamp `protobuf:"bytes,13,opt,name=finished_at,json=finishedAt,proto3" json:"finished_at,omitempty"` +} + +func (x *RepositoryDistributionTarget) Reset() { + *x = RepositoryDistributionTarget{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[114] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryDistributionTarget) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryDistributionTarget) ProtoMessage() {} + +func (x *RepositoryDistributionTarget) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[114] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryDistributionTarget.ProtoReflect.Descriptor instead. +func (*RepositoryDistributionTarget) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{114} +} + +func (x *RepositoryDistributionTarget) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *RepositoryDistributionTarget) GetTargetIp() string { + if x != nil { + return x.TargetIp + } + return "" +} + +func (x *RepositoryDistributionTarget) GetAgentId() string { + if x != nil { + return x.AgentId + } + return "" +} + +func (x *RepositoryDistributionTarget) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *RepositoryDistributionTarget) GetAttemptCount() int32 { + if x != nil { + return x.AttemptCount + } + return 0 +} + +func (x *RepositoryDistributionTarget) GetExitCode() int32 { + if x != nil { + return x.ExitCode + } + return 0 +} + +func (x *RepositoryDistributionTarget) GetOutput() string { + if x != nil { + return x.Output + } + return "" +} + +func (x *RepositoryDistributionTarget) GetOutputRef() string { + if x != nil { + return x.OutputRef + } + return "" +} + +func (x *RepositoryDistributionTarget) GetOutputTruncated() bool { + if x != nil { + return x.OutputTruncated + } + return false +} + +func (x *RepositoryDistributionTarget) GetErrorCode() string { + if x != nil { + return x.ErrorCode + } + return "" +} + +func (x *RepositoryDistributionTarget) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +func (x *RepositoryDistributionTarget) GetStartedAt() *timestamppb.Timestamp { + if x != nil { + return x.StartedAt + } + return nil +} + +func (x *RepositoryDistributionTarget) GetFinishedAt() *timestamppb.Timestamp { + if x != nil { + return x.FinishedAt + } + return nil +} + +type RepositoryDistribution struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + RepositoryId string `protobuf:"bytes,2,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + ProjectId int64 `protobuf:"varint,3,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + RevisionId string `protobuf:"bytes,4,opt,name=revision_id,json=revisionId,proto3" json:"revision_id,omitempty"` + RepositoryTaskId string `protobuf:"bytes,5,opt,name=repository_task_id,json=repositoryTaskId,proto3" json:"repository_task_id,omitempty"` + TmsTaskId string `protobuf:"bytes,6,opt,name=tms_task_id,json=tmsTaskId,proto3" json:"tms_task_id,omitempty"` + ArtifactMd5 string `protobuf:"bytes,7,opt,name=artifact_md5,json=artifactMd5,proto3" json:"artifact_md5,omitempty"` + ManifestSha256 string `protobuf:"bytes,8,opt,name=manifest_sha256,json=manifestSha256,proto3" json:"manifest_sha256,omitempty"` + RepomdSha256 string `protobuf:"bytes,9,opt,name=repomd_sha256,json=repomdSha256,proto3" json:"repomd_sha256,omitempty"` + Status string `protobuf:"bytes,10,opt,name=status,proto3" json:"status,omitempty"` + RequestedBy string `protobuf:"bytes,11,opt,name=requested_by,json=requestedBy,proto3" json:"requested_by,omitempty"` + TimeoutSeconds int32 `protobuf:"varint,12,opt,name=timeout_seconds,json=timeoutSeconds,proto3" json:"timeout_seconds,omitempty"` + ErrorCode string `protobuf:"bytes,13,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"` + ErrorMessage string `protobuf:"bytes,14,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` + Targets []*RepositoryDistributionTarget `protobuf:"bytes,15,rep,name=targets,proto3" json:"targets,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,16,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + StartedAt *timestamppb.Timestamp `protobuf:"bytes,17,opt,name=started_at,json=startedAt,proto3" json:"started_at,omitempty"` + FinishedAt *timestamppb.Timestamp `protobuf:"bytes,18,opt,name=finished_at,json=finishedAt,proto3" json:"finished_at,omitempty"` + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,19,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` +} + +func (x *RepositoryDistribution) Reset() { + *x = RepositoryDistribution{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[115] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryDistribution) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryDistribution) ProtoMessage() {} + +func (x *RepositoryDistribution) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[115] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryDistribution.ProtoReflect.Descriptor instead. +func (*RepositoryDistribution) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{115} +} + +func (x *RepositoryDistribution) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *RepositoryDistribution) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *RepositoryDistribution) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +func (x *RepositoryDistribution) GetRevisionId() string { + if x != nil { + return x.RevisionId + } + return "" +} + +func (x *RepositoryDistribution) GetRepositoryTaskId() string { + if x != nil { + return x.RepositoryTaskId + } + return "" +} + +func (x *RepositoryDistribution) GetTmsTaskId() string { + if x != nil { + return x.TmsTaskId + } + return "" +} + +func (x *RepositoryDistribution) GetArtifactMd5() string { + if x != nil { + return x.ArtifactMd5 + } + return "" +} + +func (x *RepositoryDistribution) GetManifestSha256() string { + if x != nil { + return x.ManifestSha256 + } + return "" +} + +func (x *RepositoryDistribution) GetRepomdSha256() string { + if x != nil { + return x.RepomdSha256 + } + return "" +} + +func (x *RepositoryDistribution) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *RepositoryDistribution) GetRequestedBy() string { + if x != nil { + return x.RequestedBy + } + return "" +} + +func (x *RepositoryDistribution) GetTimeoutSeconds() int32 { + if x != nil { + return x.TimeoutSeconds + } + return 0 +} + +func (x *RepositoryDistribution) GetErrorCode() string { + if x != nil { + return x.ErrorCode + } + return "" +} + +func (x *RepositoryDistribution) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +func (x *RepositoryDistribution) GetTargets() []*RepositoryDistributionTarget { + if x != nil { + return x.Targets + } + return nil +} + +func (x *RepositoryDistribution) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *RepositoryDistribution) GetStartedAt() *timestamppb.Timestamp { + if x != nil { + return x.StartedAt + } + return nil +} + +func (x *RepositoryDistribution) GetFinishedAt() *timestamppb.Timestamp { + if x != nil { + return x.FinishedAt + } + return nil +} + +func (x *RepositoryDistribution) GetUpdatedAt() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedAt + } + return nil +} + +type RepositoryDistributionResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Distribution *RepositoryDistribution `protobuf:"bytes,1,opt,name=distribution,proto3" json:"distribution,omitempty"` + Task *RepositoryTask `protobuf:"bytes,2,opt,name=task,proto3" json:"task,omitempty"` +} + +func (x *RepositoryDistributionResponse) Reset() { + *x = RepositoryDistributionResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[116] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryDistributionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryDistributionResponse) ProtoMessage() {} + +func (x *RepositoryDistributionResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[116] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryDistributionResponse.ProtoReflect.Descriptor instead. +func (*RepositoryDistributionResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{116} +} + +func (x *RepositoryDistributionResponse) GetDistribution() *RepositoryDistribution { + if x != nil { + return x.Distribution + } + return nil +} + +func (x *RepositoryDistributionResponse) GetTask() *RepositoryTask { + if x != nil { + return x.Task + } + return nil +} + +type ListRepositoryDistributionsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Distributions []*RepositoryDistribution `protobuf:"bytes,1,rep,name=distributions,proto3" json:"distributions,omitempty"` + PageResponse *common.PageResponse `protobuf:"bytes,10,opt,name=page_response,json=pageResponse,proto3" json:"page_response,omitempty"` +} + +func (x *ListRepositoryDistributionsResponse) Reset() { + *x = ListRepositoryDistributionsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[117] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListRepositoryDistributionsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRepositoryDistributionsResponse) ProtoMessage() {} + +func (x *ListRepositoryDistributionsResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[117] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRepositoryDistributionsResponse.ProtoReflect.Descriptor instead. +func (*ListRepositoryDistributionsResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{117} +} + +func (x *ListRepositoryDistributionsResponse) GetDistributions() []*RepositoryDistribution { + if x != nil { + return x.Distributions + } + return nil +} + +func (x *ListRepositoryDistributionsResponse) GetPageResponse() *common.PageResponse { + if x != nil { + return x.PageResponse + } + return nil +} + +type PreviewRepositoryPackageDeletionRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepositoryId string `protobuf:"bytes,1,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + Nevras []string `protobuf:"bytes,2,rep,name=nevras,proto3" json:"nevras,omitempty"` +} + +func (x *PreviewRepositoryPackageDeletionRequest) Reset() { + *x = PreviewRepositoryPackageDeletionRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[118] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PreviewRepositoryPackageDeletionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PreviewRepositoryPackageDeletionRequest) ProtoMessage() {} + +func (x *PreviewRepositoryPackageDeletionRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[118] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PreviewRepositoryPackageDeletionRequest.ProtoReflect.Descriptor instead. +func (*PreviewRepositoryPackageDeletionRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{118} +} + +func (x *PreviewRepositoryPackageDeletionRequest) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *PreviewRepositoryPackageDeletionRequest) GetNevras() []string { + if x != nil { + return x.Nevras + } + return nil +} + +type ConfirmRepositoryPackageDeletionRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PreviewId string `protobuf:"bytes,1,opt,name=preview_id,json=previewId,proto3" json:"preview_id,omitempty"` + PreviewDigest string `protobuf:"bytes,2,opt,name=preview_digest,json=previewDigest,proto3" json:"preview_digest,omitempty"` +} + +func (x *ConfirmRepositoryPackageDeletionRequest) Reset() { + *x = ConfirmRepositoryPackageDeletionRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[119] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ConfirmRepositoryPackageDeletionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConfirmRepositoryPackageDeletionRequest) ProtoMessage() {} + +func (x *ConfirmRepositoryPackageDeletionRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[119] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConfirmRepositoryPackageDeletionRequest.ProtoReflect.Descriptor instead. +func (*ConfirmRepositoryPackageDeletionRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{119} +} + +func (x *ConfirmRepositoryPackageDeletionRequest) GetPreviewId() string { + if x != nil { + return x.PreviewId + } + return "" +} + +func (x *ConfirmRepositoryPackageDeletionRequest) GetPreviewDigest() string { + if x != nil { + return x.PreviewDigest + } + return "" +} + +type RepositoryPackageDeletionPreviewSource struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepositoryId string `protobuf:"bytes,1,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + RevisionId string `protobuf:"bytes,2,opt,name=revision_id,json=revisionId,proto3" json:"revision_id,omitempty"` + RepoId string `protobuf:"bytes,3,opt,name=repo_id,json=repoId,proto3" json:"repo_id,omitempty"` + Baseurl string `protobuf:"bytes,4,opt,name=baseurl,proto3" json:"baseurl,omitempty"` +} + +func (x *RepositoryPackageDeletionPreviewSource) Reset() { + *x = RepositoryPackageDeletionPreviewSource{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[120] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryPackageDeletionPreviewSource) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryPackageDeletionPreviewSource) ProtoMessage() {} + +func (x *RepositoryPackageDeletionPreviewSource) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[120] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryPackageDeletionPreviewSource.ProtoReflect.Descriptor instead. +func (*RepositoryPackageDeletionPreviewSource) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{120} +} + +func (x *RepositoryPackageDeletionPreviewSource) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *RepositoryPackageDeletionPreviewSource) GetRevisionId() string { + if x != nil { + return x.RevisionId + } + return "" +} + +func (x *RepositoryPackageDeletionPreviewSource) GetRepoId() string { + if x != nil { + return x.RepoId + } + return "" +} + +func (x *RepositoryPackageDeletionPreviewSource) GetBaseurl() string { + if x != nil { + return x.Baseurl + } + return "" +} + +type RepositoryPackageDependencyImpact struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PackageNevra string `protobuf:"bytes,1,opt,name=package_nevra,json=packageNevra,proto3" json:"package_nevra,omitempty"` + Requires string `protobuf:"bytes,2,opt,name=requires,proto3" json:"requires,omitempty"` + Depth int32 `protobuf:"varint,3,opt,name=depth,proto3" json:"depth,omitempty"` + Direct bool `protobuf:"varint,4,opt,name=direct,proto3" json:"direct,omitempty"` + Reason string `protobuf:"bytes,5,opt,name=reason,proto3" json:"reason,omitempty"` +} + +func (x *RepositoryPackageDependencyImpact) Reset() { + *x = RepositoryPackageDependencyImpact{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[121] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryPackageDependencyImpact) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryPackageDependencyImpact) ProtoMessage() {} + +func (x *RepositoryPackageDependencyImpact) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[121] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryPackageDependencyImpact.ProtoReflect.Descriptor instead. +func (*RepositoryPackageDependencyImpact) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{121} +} + +func (x *RepositoryPackageDependencyImpact) GetPackageNevra() string { + if x != nil { + return x.PackageNevra + } + return "" +} + +func (x *RepositoryPackageDependencyImpact) GetRequires() string { + if x != nil { + return x.Requires + } + return "" +} + +func (x *RepositoryPackageDependencyImpact) GetDepth() int32 { + if x != nil { + return x.Depth + } + return 0 +} + +func (x *RepositoryPackageDependencyImpact) GetDirect() bool { + if x != nil { + return x.Direct + } + return false +} + +func (x *RepositoryPackageDependencyImpact) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +type RepositoryPackageDeletionPreview struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PreviewId string `protobuf:"bytes,1,opt,name=preview_id,json=previewId,proto3" json:"preview_id,omitempty"` + PreviewDigest string `protobuf:"bytes,2,opt,name=preview_digest,json=previewDigest,proto3" json:"preview_digest,omitempty"` + RepositoryId string `protobuf:"bytes,3,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + RevisionId string `protobuf:"bytes,4,opt,name=revision_id,json=revisionId,proto3" json:"revision_id,omitempty"` + RepositorySet []*RepositoryPackageDeletionPreviewSource `protobuf:"bytes,5,rep,name=repository_set,json=repositorySet,proto3" json:"repository_set,omitempty"` + Nevras []string `protobuf:"bytes,6,rep,name=nevras,proto3" json:"nevras,omitempty"` + DependencyImpact []*RepositoryPackageDependencyImpact `protobuf:"bytes,7,rep,name=dependency_impact,json=dependencyImpact,proto3" json:"dependency_impact,omitempty"` + Command []string `protobuf:"bytes,8,rep,name=command,proto3" json:"command,omitempty"` + RawOutput string `protobuf:"bytes,9,opt,name=raw_output,json=rawOutput,proto3" json:"raw_output,omitempty"` + Status string `protobuf:"bytes,10,opt,name=status,proto3" json:"status,omitempty"` + ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` + CreatedBy string `protobuf:"bytes,12,opt,name=created_by,json=createdBy,proto3" json:"created_by,omitempty"` +} + +func (x *RepositoryPackageDeletionPreview) Reset() { + *x = RepositoryPackageDeletionPreview{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[122] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryPackageDeletionPreview) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryPackageDeletionPreview) ProtoMessage() {} + +func (x *RepositoryPackageDeletionPreview) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[122] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryPackageDeletionPreview.ProtoReflect.Descriptor instead. +func (*RepositoryPackageDeletionPreview) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{122} +} + +func (x *RepositoryPackageDeletionPreview) GetPreviewId() string { + if x != nil { + return x.PreviewId + } + return "" +} + +func (x *RepositoryPackageDeletionPreview) GetPreviewDigest() string { + if x != nil { + return x.PreviewDigest + } + return "" +} + +func (x *RepositoryPackageDeletionPreview) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *RepositoryPackageDeletionPreview) GetRevisionId() string { + if x != nil { + return x.RevisionId + } + return "" +} + +func (x *RepositoryPackageDeletionPreview) GetRepositorySet() []*RepositoryPackageDeletionPreviewSource { + if x != nil { + return x.RepositorySet + } + return nil +} + +func (x *RepositoryPackageDeletionPreview) GetNevras() []string { + if x != nil { + return x.Nevras + } + return nil +} + +func (x *RepositoryPackageDeletionPreview) GetDependencyImpact() []*RepositoryPackageDependencyImpact { + if x != nil { + return x.DependencyImpact + } + return nil +} + +func (x *RepositoryPackageDeletionPreview) GetCommand() []string { + if x != nil { + return x.Command + } + return nil +} + +func (x *RepositoryPackageDeletionPreview) GetRawOutput() string { + if x != nil { + return x.RawOutput + } + return "" +} + +func (x *RepositoryPackageDeletionPreview) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *RepositoryPackageDeletionPreview) GetExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.ExpiresAt + } + return nil +} + +func (x *RepositoryPackageDeletionPreview) GetCreatedBy() string { + if x != nil { + return x.CreatedBy + } + return "" +} + +type RepositoryPackageDeletionPreviewResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Preview *RepositoryPackageDeletionPreview `protobuf:"bytes,1,opt,name=preview,proto3" json:"preview,omitempty"` +} + +func (x *RepositoryPackageDeletionPreviewResponse) Reset() { + *x = RepositoryPackageDeletionPreviewResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[123] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryPackageDeletionPreviewResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryPackageDeletionPreviewResponse) ProtoMessage() {} + +func (x *RepositoryPackageDeletionPreviewResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[123] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryPackageDeletionPreviewResponse.ProtoReflect.Descriptor instead. +func (*RepositoryPackageDeletionPreviewResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{123} +} + +func (x *RepositoryPackageDeletionPreviewResponse) GetPreview() *RepositoryPackageDeletionPreview { + if x != nil { + return x.Preview + } + return nil +} + +type PreviewRepositoryPackageInstallRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepositoryId string `protobuf:"bytes,1,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + RevisionId string `protobuf:"bytes,2,opt,name=revision_id,json=revisionId,proto3" json:"revision_id,omitempty"` + PackageNames []string `protobuf:"bytes,3,rep,name=package_names,json=packageNames,proto3" json:"package_names,omitempty"` + TargetAgentIds []string `protobuf:"bytes,4,rep,name=target_agent_ids,json=targetAgentIds,proto3" json:"target_agent_ids,omitempty"` + TimeoutSeconds int32 `protobuf:"varint,5,opt,name=timeout_seconds,json=timeoutSeconds,proto3" json:"timeout_seconds,omitempty"` +} + +func (x *PreviewRepositoryPackageInstallRequest) Reset() { + *x = PreviewRepositoryPackageInstallRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[124] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PreviewRepositoryPackageInstallRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PreviewRepositoryPackageInstallRequest) ProtoMessage() {} + +func (x *PreviewRepositoryPackageInstallRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[124] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PreviewRepositoryPackageInstallRequest.ProtoReflect.Descriptor instead. +func (*PreviewRepositoryPackageInstallRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{124} +} + +func (x *PreviewRepositoryPackageInstallRequest) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *PreviewRepositoryPackageInstallRequest) GetRevisionId() string { + if x != nil { + return x.RevisionId + } + return "" +} + +func (x *PreviewRepositoryPackageInstallRequest) GetPackageNames() []string { + if x != nil { + return x.PackageNames + } + return nil +} + +func (x *PreviewRepositoryPackageInstallRequest) GetTargetAgentIds() []string { + if x != nil { + return x.TargetAgentIds + } + return nil +} + +func (x *PreviewRepositoryPackageInstallRequest) GetTimeoutSeconds() int32 { + if x != nil { + return x.TimeoutSeconds + } + return 0 +} + +type RepositoryPackageInstallPreviewTarget struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + AgentId string `protobuf:"bytes,1,opt,name=agent_id,json=agentId,proto3" json:"agent_id,omitempty"` + Ip string `protobuf:"bytes,2,opt,name=ip,proto3" json:"ip,omitempty"` + Status RepositoryTaskStatus `protobuf:"varint,3,opt,name=status,proto3,enum=trpc.ocmanager.repository.RepositoryTaskStatus" json:"status,omitempty"` + ExitCode int32 `protobuf:"varint,4,opt,name=exit_code,json=exitCode,proto3" json:"exit_code,omitempty"` + ErrorCode string `protobuf:"bytes,5,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"` + ErrorMessage string `protobuf:"bytes,6,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` + RawOutput string `protobuf:"bytes,7,opt,name=raw_output,json=rawOutput,proto3" json:"raw_output,omitempty"` + OutputTruncated bool `protobuf:"varint,8,opt,name=output_truncated,json=outputTruncated,proto3" json:"output_truncated,omitempty"` + StartedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=started_at,json=startedAt,proto3" json:"started_at,omitempty"` + FinishedAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=finished_at,json=finishedAt,proto3" json:"finished_at,omitempty"` + SelectedPackages []string `protobuf:"bytes,11,rep,name=selected_packages,json=selectedPackages,proto3" json:"selected_packages,omitempty"` + DependencyPackages []string `protobuf:"bytes,12,rep,name=dependency_packages,json=dependencyPackages,proto3" json:"dependency_packages,omitempty"` + UpgradePackages []string `protobuf:"bytes,13,rep,name=upgrade_packages,json=upgradePackages,proto3" json:"upgrade_packages,omitempty"` + ReplacementPackages []string `protobuf:"bytes,14,rep,name=replacement_packages,json=replacementPackages,proto3" json:"replacement_packages,omitempty"` + DownloadBytes int64 `protobuf:"varint,15,opt,name=download_bytes,json=downloadBytes,proto3" json:"download_bytes,omitempty"` + DiskChangeBytes int64 `protobuf:"varint,16,opt,name=disk_change_bytes,json=diskChangeBytes,proto3" json:"disk_change_bytes,omitempty"` +} + +func (x *RepositoryPackageInstallPreviewTarget) Reset() { + *x = RepositoryPackageInstallPreviewTarget{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[125] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryPackageInstallPreviewTarget) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryPackageInstallPreviewTarget) ProtoMessage() {} + +func (x *RepositoryPackageInstallPreviewTarget) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[125] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryPackageInstallPreviewTarget.ProtoReflect.Descriptor instead. +func (*RepositoryPackageInstallPreviewTarget) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{125} +} + +func (x *RepositoryPackageInstallPreviewTarget) GetAgentId() string { + if x != nil { + return x.AgentId + } + return "" +} + +func (x *RepositoryPackageInstallPreviewTarget) GetIp() string { + if x != nil { + return x.Ip + } + return "" +} + +func (x *RepositoryPackageInstallPreviewTarget) GetStatus() RepositoryTaskStatus { + if x != nil { + return x.Status + } + return RepositoryTaskStatus_REPOSITORY_TASK_STATUS_UNSPECIFIED +} + +func (x *RepositoryPackageInstallPreviewTarget) GetExitCode() int32 { + if x != nil { + return x.ExitCode + } + return 0 +} + +func (x *RepositoryPackageInstallPreviewTarget) GetErrorCode() string { + if x != nil { + return x.ErrorCode + } + return "" +} + +func (x *RepositoryPackageInstallPreviewTarget) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +func (x *RepositoryPackageInstallPreviewTarget) GetRawOutput() string { + if x != nil { + return x.RawOutput + } + return "" +} + +func (x *RepositoryPackageInstallPreviewTarget) GetOutputTruncated() bool { + if x != nil { + return x.OutputTruncated + } + return false +} + +func (x *RepositoryPackageInstallPreviewTarget) GetStartedAt() *timestamppb.Timestamp { + if x != nil { + return x.StartedAt + } + return nil +} + +func (x *RepositoryPackageInstallPreviewTarget) GetFinishedAt() *timestamppb.Timestamp { + if x != nil { + return x.FinishedAt + } + return nil +} + +func (x *RepositoryPackageInstallPreviewTarget) GetSelectedPackages() []string { + if x != nil { + return x.SelectedPackages + } + return nil +} + +func (x *RepositoryPackageInstallPreviewTarget) GetDependencyPackages() []string { + if x != nil { + return x.DependencyPackages + } + return nil +} + +func (x *RepositoryPackageInstallPreviewTarget) GetUpgradePackages() []string { + if x != nil { + return x.UpgradePackages + } + return nil +} + +func (x *RepositoryPackageInstallPreviewTarget) GetReplacementPackages() []string { + if x != nil { + return x.ReplacementPackages + } + return nil +} + +func (x *RepositoryPackageInstallPreviewTarget) GetDownloadBytes() int64 { + if x != nil { + return x.DownloadBytes + } + return 0 +} + +func (x *RepositoryPackageInstallPreviewTarget) GetDiskChangeBytes() int64 { + if x != nil { + return x.DiskChangeBytes + } + return 0 +} + +type RepositoryPackageInstallPreview struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PreviewId string `protobuf:"bytes,1,opt,name=preview_id,json=previewId,proto3" json:"preview_id,omitempty"` + PreviewDigest string `protobuf:"bytes,2,opt,name=preview_digest,json=previewDigest,proto3" json:"preview_digest,omitempty"` + RepositoryId string `protobuf:"bytes,3,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + RevisionId string `protobuf:"bytes,4,opt,name=revision_id,json=revisionId,proto3" json:"revision_id,omitempty"` + RepoId string `protobuf:"bytes,5,opt,name=repo_id,json=repoId,proto3" json:"repo_id,omitempty"` + PackageNames []string `protobuf:"bytes,6,rep,name=package_names,json=packageNames,proto3" json:"package_names,omitempty"` + Targets []*RepositoryPackageInstallPreviewTarget `protobuf:"bytes,7,rep,name=targets,proto3" json:"targets,omitempty"` + Status string `protobuf:"bytes,8,opt,name=status,proto3" json:"status,omitempty"` + ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` + CreatedBy string `protobuf:"bytes,10,opt,name=created_by,json=createdBy,proto3" json:"created_by,omitempty"` +} + +func (x *RepositoryPackageInstallPreview) Reset() { + *x = RepositoryPackageInstallPreview{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[126] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryPackageInstallPreview) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryPackageInstallPreview) ProtoMessage() {} + +func (x *RepositoryPackageInstallPreview) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[126] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryPackageInstallPreview.ProtoReflect.Descriptor instead. +func (*RepositoryPackageInstallPreview) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{126} +} + +func (x *RepositoryPackageInstallPreview) GetPreviewId() string { + if x != nil { + return x.PreviewId + } + return "" +} + +func (x *RepositoryPackageInstallPreview) GetPreviewDigest() string { + if x != nil { + return x.PreviewDigest + } + return "" +} + +func (x *RepositoryPackageInstallPreview) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *RepositoryPackageInstallPreview) GetRevisionId() string { + if x != nil { + return x.RevisionId + } + return "" +} + +func (x *RepositoryPackageInstallPreview) GetRepoId() string { + if x != nil { + return x.RepoId + } + return "" +} + +func (x *RepositoryPackageInstallPreview) GetPackageNames() []string { + if x != nil { + return x.PackageNames + } + return nil +} + +func (x *RepositoryPackageInstallPreview) GetTargets() []*RepositoryPackageInstallPreviewTarget { + if x != nil { + return x.Targets + } + return nil +} + +func (x *RepositoryPackageInstallPreview) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *RepositoryPackageInstallPreview) GetExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.ExpiresAt + } + return nil +} + +func (x *RepositoryPackageInstallPreview) GetCreatedBy() string { + if x != nil { + return x.CreatedBy + } + return "" +} + +type RepositoryPackageInstallPreviewResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Preview *RepositoryPackageInstallPreview `protobuf:"bytes,1,opt,name=preview,proto3" json:"preview,omitempty"` +} + +func (x *RepositoryPackageInstallPreviewResponse) Reset() { + *x = RepositoryPackageInstallPreviewResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[127] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryPackageInstallPreviewResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryPackageInstallPreviewResponse) ProtoMessage() {} + +func (x *RepositoryPackageInstallPreviewResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[127] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryPackageInstallPreviewResponse.ProtoReflect.Descriptor instead. +func (*RepositoryPackageInstallPreviewResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{127} +} + +func (x *RepositoryPackageInstallPreviewResponse) GetPreview() *RepositoryPackageInstallPreview { + if x != nil { + return x.Preview + } + return nil +} + +type ConfirmRepositoryPackageInstallRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PreviewId string `protobuf:"bytes,1,opt,name=preview_id,json=previewId,proto3" json:"preview_id,omitempty"` + PreviewDigest string `protobuf:"bytes,2,opt,name=preview_digest,json=previewDigest,proto3" json:"preview_digest,omitempty"` +} + +func (x *ConfirmRepositoryPackageInstallRequest) Reset() { + *x = ConfirmRepositoryPackageInstallRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[128] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ConfirmRepositoryPackageInstallRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConfirmRepositoryPackageInstallRequest) ProtoMessage() {} + +func (x *ConfirmRepositoryPackageInstallRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[128] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConfirmRepositoryPackageInstallRequest.ProtoReflect.Descriptor instead. +func (*ConfirmRepositoryPackageInstallRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{128} +} + +func (x *ConfirmRepositoryPackageInstallRequest) GetPreviewId() string { + if x != nil { + return x.PreviewId + } + return "" +} + +func (x *ConfirmRepositoryPackageInstallRequest) GetPreviewDigest() string { + if x != nil { + return x.PreviewDigest + } + return "" +} + +type DescribeRepositoryPackageInstallRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + InstallId string `protobuf:"bytes,1,opt,name=install_id,json=installId,proto3" json:"install_id,omitempty"` +} + +func (x *DescribeRepositoryPackageInstallRequest) Reset() { + *x = DescribeRepositoryPackageInstallRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[129] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DescribeRepositoryPackageInstallRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeRepositoryPackageInstallRequest) ProtoMessage() {} + +func (x *DescribeRepositoryPackageInstallRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[129] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeRepositoryPackageInstallRequest.ProtoReflect.Descriptor instead. +func (*DescribeRepositoryPackageInstallRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{129} +} + +func (x *DescribeRepositoryPackageInstallRequest) GetInstallId() string { + if x != nil { + return x.InstallId + } + return "" +} + +type RepositoryPackageInstall struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + ProjectId int64 `protobuf:"varint,2,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + RepositoryId string `protobuf:"bytes,3,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + RevisionId string `protobuf:"bytes,4,opt,name=revision_id,json=revisionId,proto3" json:"revision_id,omitempty"` + PreviewId string `protobuf:"bytes,5,opt,name=preview_id,json=previewId,proto3" json:"preview_id,omitempty"` + RepositoryTaskId string `protobuf:"bytes,6,opt,name=repository_task_id,json=repositoryTaskId,proto3" json:"repository_task_id,omitempty"` + TmsTaskId string `protobuf:"bytes,7,opt,name=tms_task_id,json=tmsTaskId,proto3" json:"tms_task_id,omitempty"` + Status string `protobuf:"bytes,8,opt,name=status,proto3" json:"status,omitempty"` + TimeoutSeconds int32 `protobuf:"varint,9,opt,name=timeout_seconds,json=timeoutSeconds,proto3" json:"timeout_seconds,omitempty"` + RequestedBy string `protobuf:"bytes,10,opt,name=requested_by,json=requestedBy,proto3" json:"requested_by,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` +} + +func (x *RepositoryPackageInstall) Reset() { + *x = RepositoryPackageInstall{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[130] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryPackageInstall) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryPackageInstall) ProtoMessage() {} + +func (x *RepositoryPackageInstall) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[130] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryPackageInstall.ProtoReflect.Descriptor instead. +func (*RepositoryPackageInstall) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{130} +} + +func (x *RepositoryPackageInstall) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *RepositoryPackageInstall) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +func (x *RepositoryPackageInstall) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *RepositoryPackageInstall) GetRevisionId() string { + if x != nil { + return x.RevisionId + } + return "" +} + +func (x *RepositoryPackageInstall) GetPreviewId() string { + if x != nil { + return x.PreviewId + } + return "" +} + +func (x *RepositoryPackageInstall) GetRepositoryTaskId() string { + if x != nil { + return x.RepositoryTaskId + } + return "" +} + +func (x *RepositoryPackageInstall) GetTmsTaskId() string { + if x != nil { + return x.TmsTaskId + } + return "" +} + +func (x *RepositoryPackageInstall) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *RepositoryPackageInstall) GetTimeoutSeconds() int32 { + if x != nil { + return x.TimeoutSeconds + } + return 0 +} + +func (x *RepositoryPackageInstall) GetRequestedBy() string { + if x != nil { + return x.RequestedBy + } + return "" +} + +func (x *RepositoryPackageInstall) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *RepositoryPackageInstall) GetUpdatedAt() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedAt + } + return nil +} + +type RepositoryPackageInstallResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Install *RepositoryPackageInstall `protobuf:"bytes,1,opt,name=install,proto3" json:"install,omitempty"` + Task *RepositoryTask `protobuf:"bytes,2,opt,name=task,proto3" json:"task,omitempty"` +} + +func (x *RepositoryPackageInstallResponse) Reset() { + *x = RepositoryPackageInstallResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[131] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryPackageInstallResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryPackageInstallResponse) ProtoMessage() {} + +func (x *RepositoryPackageInstallResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[131] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryPackageInstallResponse.ProtoReflect.Descriptor instead. +func (*RepositoryPackageInstallResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{131} +} + +func (x *RepositoryPackageInstallResponse) GetInstall() *RepositoryPackageInstall { + if x != nil { + return x.Install + } + return nil +} + +func (x *RepositoryPackageInstallResponse) GetTask() *RepositoryTask { + if x != nil { + return x.Task + } + return nil +} + +type ExportRepositoryInventoryRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ProjectId int64 `protobuf:"varint,1,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` +} + +func (x *ExportRepositoryInventoryRequest) Reset() { + *x = ExportRepositoryInventoryRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[132] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ExportRepositoryInventoryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExportRepositoryInventoryRequest) ProtoMessage() {} + +func (x *ExportRepositoryInventoryRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[132] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExportRepositoryInventoryRequest.ProtoReflect.Descriptor instead. +func (*ExportRepositoryInventoryRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{132} +} + +func (x *ExportRepositoryInventoryRequest) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +type ExportRepositoryValidationReportRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepositoryId string `protobuf:"bytes,1,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` +} + +func (x *ExportRepositoryValidationReportRequest) Reset() { + *x = ExportRepositoryValidationReportRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[133] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ExportRepositoryValidationReportRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExportRepositoryValidationReportRequest) ProtoMessage() {} + +func (x *ExportRepositoryValidationReportRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[133] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExportRepositoryValidationReportRequest.ProtoReflect.Descriptor instead. +func (*ExportRepositoryValidationReportRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{133} +} + +func (x *ExportRepositoryValidationReportRequest) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +type ExportRepositoryDistributionResultRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + DistributionId string `protobuf:"bytes,1,opt,name=distribution_id,json=distributionId,proto3" json:"distribution_id,omitempty"` +} + +func (x *ExportRepositoryDistributionResultRequest) Reset() { + *x = ExportRepositoryDistributionResultRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[134] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ExportRepositoryDistributionResultRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExportRepositoryDistributionResultRequest) ProtoMessage() {} + +func (x *ExportRepositoryDistributionResultRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[134] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExportRepositoryDistributionResultRequest.ProtoReflect.Descriptor instead. +func (*ExportRepositoryDistributionResultRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{134} +} + +func (x *ExportRepositoryDistributionResultRequest) GetDistributionId() string { + if x != nil { + return x.DistributionId + } + return "" +} + +type RepositoryExportResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + FileName string `protobuf:"bytes,1,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"` + ContentType string `protobuf:"bytes,2,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"` + Sha256 string `protobuf:"bytes,3,opt,name=sha256,proto3" json:"sha256,omitempty"` + Content string `protobuf:"bytes,4,opt,name=content,proto3" json:"content,omitempty"` +} + +func (x *RepositoryExportResponse) Reset() { + *x = RepositoryExportResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[135] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryExportResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryExportResponse) ProtoMessage() {} + +func (x *RepositoryExportResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[135] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryExportResponse.ProtoReflect.Descriptor instead. +func (*RepositoryExportResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{135} +} + +func (x *RepositoryExportResponse) GetFileName() string { + if x != nil { + return x.FileName + } + return "" +} + +func (x *RepositoryExportResponse) GetContentType() string { + if x != nil { + return x.ContentType + } + return "" +} + +func (x *RepositoryExportResponse) GetSha256() string { + if x != nil { + return x.Sha256 + } + return "" +} + +func (x *RepositoryExportResponse) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +type RepositoryInventoryStorageNodeMapping struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SourceRef string `protobuf:"bytes,1,opt,name=source_ref,json=sourceRef,proto3" json:"source_ref,omitempty"` + TargetStorageNodeId string `protobuf:"bytes,2,opt,name=target_storage_node_id,json=targetStorageNodeId,proto3" json:"target_storage_node_id,omitempty"` +} + +func (x *RepositoryInventoryStorageNodeMapping) Reset() { + *x = RepositoryInventoryStorageNodeMapping{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[136] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryInventoryStorageNodeMapping) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryInventoryStorageNodeMapping) ProtoMessage() {} + +func (x *RepositoryInventoryStorageNodeMapping) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[136] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryInventoryStorageNodeMapping.ProtoReflect.Descriptor instead. +func (*RepositoryInventoryStorageNodeMapping) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{136} +} + +func (x *RepositoryInventoryStorageNodeMapping) GetSourceRef() string { + if x != nil { + return x.SourceRef + } + return "" +} + +func (x *RepositoryInventoryStorageNodeMapping) GetTargetStorageNodeId() string { + if x != nil { + return x.TargetStorageNodeId + } + return "" +} + +type RepositoryInventorySigningKeyMapping struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SourceKeyRef string `protobuf:"bytes,1,opt,name=source_key_ref,json=sourceKeyRef,proto3" json:"source_key_ref,omitempty"` + TargetKeyRef string `protobuf:"bytes,2,opt,name=target_key_ref,json=targetKeyRef,proto3" json:"target_key_ref,omitempty"` +} + +func (x *RepositoryInventorySigningKeyMapping) Reset() { + *x = RepositoryInventorySigningKeyMapping{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[137] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryInventorySigningKeyMapping) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryInventorySigningKeyMapping) ProtoMessage() {} + +func (x *RepositoryInventorySigningKeyMapping) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[137] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryInventorySigningKeyMapping.ProtoReflect.Descriptor instead. +func (*RepositoryInventorySigningKeyMapping) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{137} +} + +func (x *RepositoryInventorySigningKeyMapping) GetSourceKeyRef() string { + if x != nil { + return x.SourceKeyRef + } + return "" +} + +func (x *RepositoryInventorySigningKeyMapping) GetTargetKeyRef() string { + if x != nil { + return x.TargetKeyRef + } + return "" +} + +type PreviewRepositoryInventoryImportRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ProjectId int64 `protobuf:"varint,1,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + Content string `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"` + ConflictStrategy RepositoryInventoryConflictStrategy `protobuf:"varint,3,opt,name=conflict_strategy,json=conflictStrategy,proto3,enum=trpc.ocmanager.repository.RepositoryInventoryConflictStrategy" json:"conflict_strategy,omitempty"` + DefaultStorageNodeId string `protobuf:"bytes,4,opt,name=default_storage_node_id,json=defaultStorageNodeId,proto3" json:"default_storage_node_id,omitempty"` + StorageNodeMappings []*RepositoryInventoryStorageNodeMapping `protobuf:"bytes,5,rep,name=storage_node_mappings,json=storageNodeMappings,proto3" json:"storage_node_mappings,omitempty"` + SigningKeyMappings []*RepositoryInventorySigningKeyMapping `protobuf:"bytes,6,rep,name=signing_key_mappings,json=signingKeyMappings,proto3" json:"signing_key_mappings,omitempty"` +} + +func (x *PreviewRepositoryInventoryImportRequest) Reset() { + *x = PreviewRepositoryInventoryImportRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[138] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PreviewRepositoryInventoryImportRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PreviewRepositoryInventoryImportRequest) ProtoMessage() {} + +func (x *PreviewRepositoryInventoryImportRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[138] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PreviewRepositoryInventoryImportRequest.ProtoReflect.Descriptor instead. +func (*PreviewRepositoryInventoryImportRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{138} +} + +func (x *PreviewRepositoryInventoryImportRequest) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +func (x *PreviewRepositoryInventoryImportRequest) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +func (x *PreviewRepositoryInventoryImportRequest) GetConflictStrategy() RepositoryInventoryConflictStrategy { + if x != nil { + return x.ConflictStrategy + } + return RepositoryInventoryConflictStrategy_REPOSITORY_INVENTORY_CONFLICT_STRATEGY_UNSPECIFIED +} + +func (x *PreviewRepositoryInventoryImportRequest) GetDefaultStorageNodeId() string { + if x != nil { + return x.DefaultStorageNodeId + } + return "" +} + +func (x *PreviewRepositoryInventoryImportRequest) GetStorageNodeMappings() []*RepositoryInventoryStorageNodeMapping { + if x != nil { + return x.StorageNodeMappings + } + return nil +} + +func (x *PreviewRepositoryInventoryImportRequest) GetSigningKeyMappings() []*RepositoryInventorySigningKeyMapping { + if x != nil { + return x.SigningKeyMappings + } + return nil +} + +type RepositoryInventoryImportItem struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SourceRepositoryId string `protobuf:"bytes,1,opt,name=source_repository_id,json=sourceRepositoryId,proto3" json:"source_repository_id,omitempty"` + SourceName string `protobuf:"bytes,2,opt,name=source_name,json=sourceName,proto3" json:"source_name,omitempty"` + Action string `protobuf:"bytes,3,opt,name=action,proto3" json:"action,omitempty"` + Message string `protobuf:"bytes,4,opt,name=message,proto3" json:"message,omitempty"` + Spec *RepositorySpec `protobuf:"bytes,5,opt,name=spec,proto3" json:"spec,omitempty"` +} + +func (x *RepositoryInventoryImportItem) Reset() { + *x = RepositoryInventoryImportItem{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[139] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryInventoryImportItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryInventoryImportItem) ProtoMessage() {} + +func (x *RepositoryInventoryImportItem) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[139] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryInventoryImportItem.ProtoReflect.Descriptor instead. +func (*RepositoryInventoryImportItem) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{139} +} + +func (x *RepositoryInventoryImportItem) GetSourceRepositoryId() string { + if x != nil { + return x.SourceRepositoryId + } + return "" +} + +func (x *RepositoryInventoryImportItem) GetSourceName() string { + if x != nil { + return x.SourceName + } + return "" +} + +func (x *RepositoryInventoryImportItem) GetAction() string { + if x != nil { + return x.Action + } + return "" +} + +func (x *RepositoryInventoryImportItem) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *RepositoryInventoryImportItem) GetSpec() *RepositorySpec { + if x != nil { + return x.Spec + } + return nil +} + +type RepositoryInventoryImportPreviewResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PreviewId string `protobuf:"bytes,1,opt,name=preview_id,json=previewId,proto3" json:"preview_id,omitempty"` + SchemaVersion string `protobuf:"bytes,2,opt,name=schema_version,json=schemaVersion,proto3" json:"schema_version,omitempty"` + PreviewDigest string `protobuf:"bytes,3,opt,name=preview_digest,json=previewDigest,proto3" json:"preview_digest,omitempty"` + ResourceVersion string `protobuf:"bytes,4,opt,name=resource_version,json=resourceVersion,proto3" json:"resource_version,omitempty"` + ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` + Status string `protobuf:"bytes,6,opt,name=status,proto3" json:"status,omitempty"` + Items []*RepositoryInventoryImportItem `protobuf:"bytes,7,rep,name=items,proto3" json:"items,omitempty"` + Warnings []string `protobuf:"bytes,8,rep,name=warnings,proto3" json:"warnings,omitempty"` +} + +func (x *RepositoryInventoryImportPreviewResponse) Reset() { + *x = RepositoryInventoryImportPreviewResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[140] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryInventoryImportPreviewResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryInventoryImportPreviewResponse) ProtoMessage() {} + +func (x *RepositoryInventoryImportPreviewResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[140] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryInventoryImportPreviewResponse.ProtoReflect.Descriptor instead. +func (*RepositoryInventoryImportPreviewResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{140} +} + +func (x *RepositoryInventoryImportPreviewResponse) GetPreviewId() string { + if x != nil { + return x.PreviewId + } + return "" +} + +func (x *RepositoryInventoryImportPreviewResponse) GetSchemaVersion() string { + if x != nil { + return x.SchemaVersion + } + return "" +} + +func (x *RepositoryInventoryImportPreviewResponse) GetPreviewDigest() string { + if x != nil { + return x.PreviewDigest + } + return "" +} + +func (x *RepositoryInventoryImportPreviewResponse) GetResourceVersion() string { + if x != nil { + return x.ResourceVersion + } + return "" +} + +func (x *RepositoryInventoryImportPreviewResponse) GetExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.ExpiresAt + } + return nil +} + +func (x *RepositoryInventoryImportPreviewResponse) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *RepositoryInventoryImportPreviewResponse) GetItems() []*RepositoryInventoryImportItem { + if x != nil { + return x.Items + } + return nil +} + +func (x *RepositoryInventoryImportPreviewResponse) GetWarnings() []string { + if x != nil { + return x.Warnings + } + return nil +} + +type ConfirmRepositoryInventoryImportRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PreviewId string `protobuf:"bytes,1,opt,name=preview_id,json=previewId,proto3" json:"preview_id,omitempty"` + PreviewDigest string `protobuf:"bytes,2,opt,name=preview_digest,json=previewDigest,proto3" json:"preview_digest,omitempty"` +} + +func (x *ConfirmRepositoryInventoryImportRequest) Reset() { + *x = ConfirmRepositoryInventoryImportRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[141] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ConfirmRepositoryInventoryImportRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConfirmRepositoryInventoryImportRequest) ProtoMessage() {} + +func (x *ConfirmRepositoryInventoryImportRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[141] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConfirmRepositoryInventoryImportRequest.ProtoReflect.Descriptor instead. +func (*ConfirmRepositoryInventoryImportRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{141} +} + +func (x *ConfirmRepositoryInventoryImportRequest) GetPreviewId() string { + if x != nil { + return x.PreviewId + } + return "" +} + +func (x *ConfirmRepositoryInventoryImportRequest) GetPreviewDigest() string { + if x != nil { + return x.PreviewDigest + } + return "" +} + +type RepositoryInventoryImportJob struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + ProjectId int64 `protobuf:"varint,2,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + PreviewId string `protobuf:"bytes,3,opt,name=preview_id,json=previewId,proto3" json:"preview_id,omitempty"` + SchemaVersion string `protobuf:"bytes,4,opt,name=schema_version,json=schemaVersion,proto3" json:"schema_version,omitempty"` + ConflictStrategy string `protobuf:"bytes,5,opt,name=conflict_strategy,json=conflictStrategy,proto3" json:"conflict_strategy,omitempty"` + Status string `protobuf:"bytes,6,opt,name=status,proto3" json:"status,omitempty"` + ErrorCode string `protobuf:"bytes,7,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"` + ErrorMessage string `protobuf:"bytes,8,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` + RequestedBy string `protobuf:"bytes,9,opt,name=requested_by,json=requestedBy,proto3" json:"requested_by,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + FinishedAt *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=finished_at,json=finishedAt,proto3" json:"finished_at,omitempty"` + RepositoryTaskId string `protobuf:"bytes,12,opt,name=repository_task_id,json=repositoryTaskId,proto3" json:"repository_task_id,omitempty"` +} + +func (x *RepositoryInventoryImportJob) Reset() { + *x = RepositoryInventoryImportJob{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[142] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryInventoryImportJob) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryInventoryImportJob) ProtoMessage() {} + +func (x *RepositoryInventoryImportJob) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[142] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryInventoryImportJob.ProtoReflect.Descriptor instead. +func (*RepositoryInventoryImportJob) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{142} +} + +func (x *RepositoryInventoryImportJob) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *RepositoryInventoryImportJob) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +func (x *RepositoryInventoryImportJob) GetPreviewId() string { + if x != nil { + return x.PreviewId + } + return "" +} + +func (x *RepositoryInventoryImportJob) GetSchemaVersion() string { + if x != nil { + return x.SchemaVersion + } + return "" +} + +func (x *RepositoryInventoryImportJob) GetConflictStrategy() string { + if x != nil { + return x.ConflictStrategy + } + return "" +} + +func (x *RepositoryInventoryImportJob) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *RepositoryInventoryImportJob) GetErrorCode() string { + if x != nil { + return x.ErrorCode + } + return "" +} + +func (x *RepositoryInventoryImportJob) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +func (x *RepositoryInventoryImportJob) GetRequestedBy() string { + if x != nil { + return x.RequestedBy + } + return "" +} + +func (x *RepositoryInventoryImportJob) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *RepositoryInventoryImportJob) GetFinishedAt() *timestamppb.Timestamp { + if x != nil { + return x.FinishedAt + } + return nil +} + +func (x *RepositoryInventoryImportJob) GetRepositoryTaskId() string { + if x != nil { + return x.RepositoryTaskId + } + return "" +} + +type RepositoryInventoryImportResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Job *RepositoryInventoryImportJob `protobuf:"bytes,1,opt,name=job,proto3" json:"job,omitempty"` + Repositories []*Repository `protobuf:"bytes,2,rep,name=repositories,proto3" json:"repositories,omitempty"` +} + +func (x *RepositoryInventoryImportResponse) Reset() { + *x = RepositoryInventoryImportResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[143] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryInventoryImportResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryInventoryImportResponse) ProtoMessage() {} + +func (x *RepositoryInventoryImportResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[143] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryInventoryImportResponse.ProtoReflect.Descriptor instead. +func (*RepositoryInventoryImportResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{143} +} + +func (x *RepositoryInventoryImportResponse) GetJob() *RepositoryInventoryImportJob { + if x != nil { + return x.Job + } + return nil +} + +func (x *RepositoryInventoryImportResponse) GetRepositories() []*Repository { + if x != nil { + return x.Repositories + } + return nil +} + +// Kept at the end so adding the migration API does not renumber existing +// generated message indexes. +type MigrateRepositoryRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepositoryId string `protobuf:"bytes,1,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + TargetStorageNodeId string `protobuf:"bytes,2,opt,name=target_storage_node_id,json=targetStorageNodeId,proto3" json:"target_storage_node_id,omitempty"` +} + +func (x *MigrateRepositoryRequest) Reset() { + *x = MigrateRepositoryRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[144] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MigrateRepositoryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MigrateRepositoryRequest) ProtoMessage() {} + +func (x *MigrateRepositoryRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[144] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MigrateRepositoryRequest.ProtoReflect.Descriptor instead. +func (*MigrateRepositoryRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{144} +} + +func (x *MigrateRepositoryRequest) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *MigrateRepositoryRequest) GetTargetStorageNodeId() string { + if x != nil { + return x.TargetStorageNodeId + } + return "" +} + +type DescribeRepositoryTaskLogRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` +} + +func (x *DescribeRepositoryTaskLogRequest) Reset() { + *x = DescribeRepositoryTaskLogRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[145] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DescribeRepositoryTaskLogRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeRepositoryTaskLogRequest) ProtoMessage() {} + +func (x *DescribeRepositoryTaskLogRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[145] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeRepositoryTaskLogRequest.ProtoReflect.Descriptor instead. +func (*DescribeRepositoryTaskLogRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{145} +} + +func (x *DescribeRepositoryTaskLogRequest) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +type DownloadRepositoryTaskLogRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` +} + +func (x *DownloadRepositoryTaskLogRequest) Reset() { + *x = DownloadRepositoryTaskLogRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[146] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DownloadRepositoryTaskLogRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DownloadRepositoryTaskLogRequest) ProtoMessage() {} + +func (x *DownloadRepositoryTaskLogRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[146] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DownloadRepositoryTaskLogRequest.ProtoReflect.Descriptor instead. +func (*DownloadRepositoryTaskLogRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{146} +} + +func (x *DownloadRepositoryTaskLogRequest) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +type RepositoryTaskLog struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + StorageNodeId string `protobuf:"bytes,2,opt,name=storage_node_id,json=storageNodeId,proto3" json:"storage_node_id,omitempty"` + Sha256 string `protobuf:"bytes,3,opt,name=sha256,proto3" json:"sha256,omitempty"` + CompressedBytes int64 `protobuf:"varint,4,opt,name=compressed_bytes,json=compressedBytes,proto3" json:"compressed_bytes,omitempty"` + UncompressedBytes int64 `protobuf:"varint,5,opt,name=uncompressed_bytes,json=uncompressedBytes,proto3" json:"uncompressed_bytes,omitempty"` + LineCount int64 `protobuf:"varint,6,opt,name=line_count,json=lineCount,proto3" json:"line_count,omitempty"` + Summary string `protobuf:"bytes,7,opt,name=summary,proto3" json:"summary,omitempty"` + ErrorFingerprint string `protobuf:"bytes,8,opt,name=error_fingerprint,json=errorFingerprint,proto3" json:"error_fingerprint,omitempty"` + ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` +} + +func (x *RepositoryTaskLog) Reset() { + *x = RepositoryTaskLog{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[147] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryTaskLog) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryTaskLog) ProtoMessage() {} + +func (x *RepositoryTaskLog) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[147] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryTaskLog.ProtoReflect.Descriptor instead. +func (*RepositoryTaskLog) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{147} +} + +func (x *RepositoryTaskLog) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *RepositoryTaskLog) GetStorageNodeId() string { + if x != nil { + return x.StorageNodeId + } + return "" +} + +func (x *RepositoryTaskLog) GetSha256() string { + if x != nil { + return x.Sha256 + } + return "" +} + +func (x *RepositoryTaskLog) GetCompressedBytes() int64 { + if x != nil { + return x.CompressedBytes + } + return 0 +} + +func (x *RepositoryTaskLog) GetUncompressedBytes() int64 { + if x != nil { + return x.UncompressedBytes + } + return 0 +} + +func (x *RepositoryTaskLog) GetLineCount() int64 { + if x != nil { + return x.LineCount + } + return 0 +} + +func (x *RepositoryTaskLog) GetSummary() string { + if x != nil { + return x.Summary + } + return "" +} + +func (x *RepositoryTaskLog) GetErrorFingerprint() string { + if x != nil { + return x.ErrorFingerprint + } + return "" +} + +func (x *RepositoryTaskLog) GetExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.ExpiresAt + } + return nil +} + +func (x *RepositoryTaskLog) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +type RepositoryTaskLogResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TaskLog *RepositoryTaskLog `protobuf:"bytes,1,opt,name=task_log,json=taskLog,proto3" json:"task_log,omitempty"` +} + +func (x *RepositoryTaskLogResponse) Reset() { + *x = RepositoryTaskLogResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[148] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryTaskLogResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryTaskLogResponse) ProtoMessage() {} + +func (x *RepositoryTaskLogResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[148] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryTaskLogResponse.ProtoReflect.Descriptor instead. +func (*RepositoryTaskLogResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{148} +} + +func (x *RepositoryTaskLogResponse) GetTaskLog() *RepositoryTaskLog { + if x != nil { + return x.TaskLog + } + return nil +} + +// Upstream profiles are reusable form templates. Repository creation resolves +// them into the existing RepositorySpec fields and does not persist a profile +// dependency in the repository runtime model. +type RepositoryUpstreamProfileSpec struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ProjectId int64 `protobuf:"varint,1,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + SystemProduct string `protobuf:"bytes,4,opt,name=system_product,json=systemProduct,proto3" json:"system_product,omitempty"` + Channel string `protobuf:"bytes,5,opt,name=channel,proto3" json:"channel,omitempty"` + UrlTemplate string `protobuf:"bytes,6,opt,name=url_template,json=urlTemplate,proto3" json:"url_template,omitempty"` + SupportedVersions []string `protobuf:"bytes,7,rep,name=supported_versions,json=supportedVersions,proto3" json:"supported_versions,omitempty"` + SupportedArchitectures []RepositoryArchitecture `protobuf:"varint,8,rep,packed,name=supported_architectures,json=supportedArchitectures,proto3,enum=trpc.ocmanager.repository.RepositoryArchitecture" json:"supported_architectures,omitempty"` + DefaultIncludePackages []string `protobuf:"bytes,9,rep,name=default_include_packages,json=defaultIncludePackages,proto3" json:"default_include_packages,omitempty"` + DefaultExcludePackages []string `protobuf:"bytes,10,rep,name=default_exclude_packages,json=defaultExcludePackages,proto3" json:"default_exclude_packages,omitempty"` + Enabled *bool `protobuf:"varint,11,opt,name=enabled,proto3,oneof" json:"enabled,omitempty"` +} + +func (x *RepositoryUpstreamProfileSpec) Reset() { + *x = RepositoryUpstreamProfileSpec{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[149] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryUpstreamProfileSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryUpstreamProfileSpec) ProtoMessage() {} + +func (x *RepositoryUpstreamProfileSpec) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[149] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryUpstreamProfileSpec.ProtoReflect.Descriptor instead. +func (*RepositoryUpstreamProfileSpec) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{149} +} + +func (x *RepositoryUpstreamProfileSpec) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +func (x *RepositoryUpstreamProfileSpec) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *RepositoryUpstreamProfileSpec) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *RepositoryUpstreamProfileSpec) GetSystemProduct() string { + if x != nil { + return x.SystemProduct + } + return "" +} + +func (x *RepositoryUpstreamProfileSpec) GetChannel() string { + if x != nil { + return x.Channel + } + return "" +} + +func (x *RepositoryUpstreamProfileSpec) GetUrlTemplate() string { + if x != nil { + return x.UrlTemplate + } + return "" +} + +func (x *RepositoryUpstreamProfileSpec) GetSupportedVersions() []string { + if x != nil { + return x.SupportedVersions + } + return nil +} + +func (x *RepositoryUpstreamProfileSpec) GetSupportedArchitectures() []RepositoryArchitecture { + if x != nil { + return x.SupportedArchitectures + } + return nil +} + +func (x *RepositoryUpstreamProfileSpec) GetDefaultIncludePackages() []string { + if x != nil { + return x.DefaultIncludePackages + } + return nil +} + +func (x *RepositoryUpstreamProfileSpec) GetDefaultExcludePackages() []string { + if x != nil { + return x.DefaultExcludePackages + } + return nil +} + +func (x *RepositoryUpstreamProfileSpec) GetEnabled() bool { + if x != nil && x.Enabled != nil { + return *x.Enabled + } + return false +} + +type RepositoryUpstreamProfile struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + ProjectId int64 `protobuf:"varint,2,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` + SystemProduct string `protobuf:"bytes,5,opt,name=system_product,json=systemProduct,proto3" json:"system_product,omitempty"` + Channel string `protobuf:"bytes,6,opt,name=channel,proto3" json:"channel,omitempty"` + UrlTemplate string `protobuf:"bytes,7,opt,name=url_template,json=urlTemplate,proto3" json:"url_template,omitempty"` + SupportedVersions []string `protobuf:"bytes,8,rep,name=supported_versions,json=supportedVersions,proto3" json:"supported_versions,omitempty"` + SupportedArchitectures []RepositoryArchitecture `protobuf:"varint,9,rep,packed,name=supported_architectures,json=supportedArchitectures,proto3,enum=trpc.ocmanager.repository.RepositoryArchitecture" json:"supported_architectures,omitempty"` + DefaultIncludePackages []string `protobuf:"bytes,10,rep,name=default_include_packages,json=defaultIncludePackages,proto3" json:"default_include_packages,omitempty"` + DefaultExcludePackages []string `protobuf:"bytes,11,rep,name=default_exclude_packages,json=defaultExcludePackages,proto3" json:"default_exclude_packages,omitempty"` + Enabled bool `protobuf:"varint,12,opt,name=enabled,proto3" json:"enabled,omitempty"` + CreatedBy string `protobuf:"bytes,13,opt,name=created_by,json=createdBy,proto3" json:"created_by,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,14,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,15,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` +} + +func (x *RepositoryUpstreamProfile) Reset() { + *x = RepositoryUpstreamProfile{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[150] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryUpstreamProfile) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryUpstreamProfile) ProtoMessage() {} + +func (x *RepositoryUpstreamProfile) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[150] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryUpstreamProfile.ProtoReflect.Descriptor instead. +func (*RepositoryUpstreamProfile) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{150} +} + +func (x *RepositoryUpstreamProfile) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *RepositoryUpstreamProfile) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +func (x *RepositoryUpstreamProfile) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *RepositoryUpstreamProfile) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *RepositoryUpstreamProfile) GetSystemProduct() string { + if x != nil { + return x.SystemProduct + } + return "" +} + +func (x *RepositoryUpstreamProfile) GetChannel() string { + if x != nil { + return x.Channel + } + return "" +} + +func (x *RepositoryUpstreamProfile) GetUrlTemplate() string { + if x != nil { + return x.UrlTemplate + } + return "" +} + +func (x *RepositoryUpstreamProfile) GetSupportedVersions() []string { + if x != nil { + return x.SupportedVersions + } + return nil +} + +func (x *RepositoryUpstreamProfile) GetSupportedArchitectures() []RepositoryArchitecture { + if x != nil { + return x.SupportedArchitectures + } + return nil +} + +func (x *RepositoryUpstreamProfile) GetDefaultIncludePackages() []string { + if x != nil { + return x.DefaultIncludePackages + } + return nil +} + +func (x *RepositoryUpstreamProfile) GetDefaultExcludePackages() []string { + if x != nil { + return x.DefaultExcludePackages + } + return nil +} + +func (x *RepositoryUpstreamProfile) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +func (x *RepositoryUpstreamProfile) GetCreatedBy() string { + if x != nil { + return x.CreatedBy + } + return "" +} + +func (x *RepositoryUpstreamProfile) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *RepositoryUpstreamProfile) GetUpdatedAt() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedAt + } + return nil +} + +type CreateRepositoryUpstreamProfileRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Spec *RepositoryUpstreamProfileSpec `protobuf:"bytes,1,opt,name=spec,proto3" json:"spec,omitempty"` +} + +func (x *CreateRepositoryUpstreamProfileRequest) Reset() { + *x = CreateRepositoryUpstreamProfileRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[151] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateRepositoryUpstreamProfileRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateRepositoryUpstreamProfileRequest) ProtoMessage() {} + +func (x *CreateRepositoryUpstreamProfileRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[151] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateRepositoryUpstreamProfileRequest.ProtoReflect.Descriptor instead. +func (*CreateRepositoryUpstreamProfileRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{151} +} + +func (x *CreateRepositoryUpstreamProfileRequest) GetSpec() *RepositoryUpstreamProfileSpec { + if x != nil { + return x.Spec + } + return nil +} + +type UpdateRepositoryUpstreamProfileRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + UpstreamProfileId string `protobuf:"bytes,1,opt,name=upstream_profile_id,json=upstreamProfileId,proto3" json:"upstream_profile_id,omitempty"` + Spec *RepositoryUpstreamProfileSpec `protobuf:"bytes,2,opt,name=spec,proto3" json:"spec,omitempty"` +} + +func (x *UpdateRepositoryUpstreamProfileRequest) Reset() { + *x = UpdateRepositoryUpstreamProfileRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[152] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UpdateRepositoryUpstreamProfileRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateRepositoryUpstreamProfileRequest) ProtoMessage() {} + +func (x *UpdateRepositoryUpstreamProfileRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[152] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateRepositoryUpstreamProfileRequest.ProtoReflect.Descriptor instead. +func (*UpdateRepositoryUpstreamProfileRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{152} +} + +func (x *UpdateRepositoryUpstreamProfileRequest) GetUpstreamProfileId() string { + if x != nil { + return x.UpstreamProfileId + } + return "" +} + +func (x *UpdateRepositoryUpstreamProfileRequest) GetSpec() *RepositoryUpstreamProfileSpec { + if x != nil { + return x.Spec + } + return nil +} + +type DescribeRepositoryUpstreamProfileRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + UpstreamProfileId string `protobuf:"bytes,1,opt,name=upstream_profile_id,json=upstreamProfileId,proto3" json:"upstream_profile_id,omitempty"` +} + +func (x *DescribeRepositoryUpstreamProfileRequest) Reset() { + *x = DescribeRepositoryUpstreamProfileRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[153] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DescribeRepositoryUpstreamProfileRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeRepositoryUpstreamProfileRequest) ProtoMessage() {} + +func (x *DescribeRepositoryUpstreamProfileRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[153] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeRepositoryUpstreamProfileRequest.ProtoReflect.Descriptor instead. +func (*DescribeRepositoryUpstreamProfileRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{153} +} + +func (x *DescribeRepositoryUpstreamProfileRequest) GetUpstreamProfileId() string { + if x != nil { + return x.UpstreamProfileId + } + return "" +} + +type ListRepositoryUpstreamProfilesRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ProjectId int64 `protobuf:"varint,1,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + SystemProduct string `protobuf:"bytes,2,opt,name=system_product,json=systemProduct,proto3" json:"system_product,omitempty"` + Keyword string `protobuf:"bytes,3,opt,name=keyword,proto3" json:"keyword,omitempty"` + Enabled *bool `protobuf:"varint,4,opt,name=enabled,proto3,oneof" json:"enabled,omitempty"` + PageRequest *common.PageRequest `protobuf:"bytes,10,opt,name=page_request,json=pageRequest,proto3" json:"page_request,omitempty"` +} + +func (x *ListRepositoryUpstreamProfilesRequest) Reset() { + *x = ListRepositoryUpstreamProfilesRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[154] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListRepositoryUpstreamProfilesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRepositoryUpstreamProfilesRequest) ProtoMessage() {} + +func (x *ListRepositoryUpstreamProfilesRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[154] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRepositoryUpstreamProfilesRequest.ProtoReflect.Descriptor instead. +func (*ListRepositoryUpstreamProfilesRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{154} +} + +func (x *ListRepositoryUpstreamProfilesRequest) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +func (x *ListRepositoryUpstreamProfilesRequest) GetSystemProduct() string { + if x != nil { + return x.SystemProduct + } + return "" +} + +func (x *ListRepositoryUpstreamProfilesRequest) GetKeyword() string { + if x != nil { + return x.Keyword + } + return "" +} + +func (x *ListRepositoryUpstreamProfilesRequest) GetEnabled() bool { + if x != nil && x.Enabled != nil { + return *x.Enabled + } + return false +} + +func (x *ListRepositoryUpstreamProfilesRequest) GetPageRequest() *common.PageRequest { + if x != nil { + return x.PageRequest + } + return nil +} + +type DeleteRepositoryUpstreamProfileRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + UpstreamProfileId string `protobuf:"bytes,1,opt,name=upstream_profile_id,json=upstreamProfileId,proto3" json:"upstream_profile_id,omitempty"` +} + +func (x *DeleteRepositoryUpstreamProfileRequest) Reset() { + *x = DeleteRepositoryUpstreamProfileRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[155] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteRepositoryUpstreamProfileRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteRepositoryUpstreamProfileRequest) ProtoMessage() {} + +func (x *DeleteRepositoryUpstreamProfileRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[155] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteRepositoryUpstreamProfileRequest.ProtoReflect.Descriptor instead. +func (*DeleteRepositoryUpstreamProfileRequest) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{155} +} + +func (x *DeleteRepositoryUpstreamProfileRequest) GetUpstreamProfileId() string { + if x != nil { + return x.UpstreamProfileId + } + return "" +} + +type RepositoryUpstreamProfileResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Profile *RepositoryUpstreamProfile `protobuf:"bytes,1,opt,name=profile,proto3" json:"profile,omitempty"` +} + +func (x *RepositoryUpstreamProfileResponse) Reset() { + *x = RepositoryUpstreamProfileResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[156] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryUpstreamProfileResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryUpstreamProfileResponse) ProtoMessage() {} + +func (x *RepositoryUpstreamProfileResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[156] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryUpstreamProfileResponse.ProtoReflect.Descriptor instead. +func (*RepositoryUpstreamProfileResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{156} +} + +func (x *RepositoryUpstreamProfileResponse) GetProfile() *RepositoryUpstreamProfile { + if x != nil { + return x.Profile + } + return nil +} + +type ListRepositoryUpstreamProfilesResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Profiles []*RepositoryUpstreamProfile `protobuf:"bytes,1,rep,name=profiles,proto3" json:"profiles,omitempty"` + PageResponse *common.PageResponse `protobuf:"bytes,10,opt,name=page_response,json=pageResponse,proto3" json:"page_response,omitempty"` +} + +func (x *ListRepositoryUpstreamProfilesResponse) Reset() { + *x = ListRepositoryUpstreamProfilesResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_repository_proto_msgTypes[157] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListRepositoryUpstreamProfilesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRepositoryUpstreamProfilesResponse) ProtoMessage() {} + +func (x *ListRepositoryUpstreamProfilesResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_repository_proto_msgTypes[157] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRepositoryUpstreamProfilesResponse.ProtoReflect.Descriptor instead. +func (*ListRepositoryUpstreamProfilesResponse) Descriptor() ([]byte, []int) { + return file_repository_repository_proto_rawDescGZIP(), []int{157} +} + +func (x *ListRepositoryUpstreamProfilesResponse) GetProfiles() []*RepositoryUpstreamProfile { + if x != nil { + return x.Profiles + } + return nil +} + +func (x *ListRepositoryUpstreamProfilesResponse) GetPageResponse() *common.PageResponse { + if x != nil { + return x.PageResponse + } + return nil +} + +var File_repository_repository_proto protoreflect.FileDescriptor + +var file_repository_repository_proto_rawDesc = []byte{ + 0x0a, 0x1b, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2f, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x19, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x1a, 0x13, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, + 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0e, 0x76, 0x61, 0x6c, + 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xf0, 0x07, 0x0a, 0x0e, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x70, 0x65, 0x63, 0x12, 0x26, + 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x03, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x09, 0x70, 0x72, 0x6f, + 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x31, 0x0a, 0x0e, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, + 0x5f, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, + 0xfa, 0x42, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0x80, 0x01, 0x52, 0x0d, 0x73, 0x79, 0x73, 0x74, + 0x65, 0x6d, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x12, 0x32, 0x0a, 0x0f, 0x70, 0x72, 0x6f, + 0x64, 0x75, 0x63, 0x74, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x0e, 0x70, + 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x0a, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xfa, 0x42, 0x07, + 0x72, 0x05, 0x10, 0x01, 0x18, 0x80, 0x01, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2a, 0x0a, + 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0x18, 0x80, 0x08, 0x52, 0x0b, 0x64, 0x65, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x55, 0x0a, 0x0c, 0x61, 0x72, 0x63, + 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x31, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, + 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x41, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, + 0x72, 0x65, 0x52, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, + 0x12, 0x3d, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x29, + 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, + 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, + 0x2b, 0x0a, 0x0c, 0x75, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x75, 0x72, 0x6c, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0x18, 0x80, 0x10, 0x52, + 0x0b, 0x75, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x55, 0x72, 0x6c, 0x12, 0x21, 0x0a, 0x0c, + 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x09, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x0b, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x12, + 0x29, 0x0a, 0x10, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x61, + 0x67, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x69, 0x6e, 0x63, 0x6c, 0x75, + 0x64, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x65, 0x78, + 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x18, 0x0b, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x50, 0x61, 0x63, + 0x6b, 0x61, 0x67, 0x65, 0x73, 0x12, 0x4d, 0x0a, 0x0a, 0x67, 0x70, 0x67, 0x5f, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2e, 0x2e, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x47, 0x50, 0x47, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x09, 0x67, 0x70, 0x67, 0x50, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x12, 0x1d, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, + 0x0d, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, + 0x88, 0x01, 0x01, 0x12, 0x40, 0x0a, 0x17, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x5f, 0x73, + 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x0e, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, + 0x14, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, + 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x4a, 0x0a, 0x09, 0x73, 0x79, 0x6e, 0x63, 0x5f, 0x6d, 0x6f, + 0x64, 0x65, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2d, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, + 0x79, 0x6e, 0x63, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x08, 0x73, 0x79, 0x6e, 0x63, 0x4d, 0x6f, 0x64, + 0x65, 0x12, 0x3b, 0x0a, 0x15, 0x62, 0x61, 0x6e, 0x64, 0x77, 0x69, 0x64, 0x74, 0x68, 0x5f, 0x6c, + 0x69, 0x6d, 0x69, 0x74, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x10, 0x20, 0x01, 0x28, 0x03, + 0x42, 0x07, 0xfa, 0x42, 0x04, 0x22, 0x02, 0x28, 0x00, 0x52, 0x13, 0x62, 0x61, 0x6e, 0x64, 0x77, + 0x69, 0x64, 0x74, 0x68, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x3f, + 0x0a, 0x16, 0x6d, 0x61, 0x78, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6c, 0x6c, 0x65, 0x6c, 0x5f, 0x64, + 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x18, 0x11, 0x20, 0x01, 0x28, 0x05, 0x42, 0x09, + 0xfa, 0x42, 0x06, 0x1a, 0x04, 0x18, 0x14, 0x28, 0x00, 0x52, 0x14, 0x6d, 0x61, 0x78, 0x50, 0x61, + 0x72, 0x61, 0x6c, 0x6c, 0x65, 0x6c, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x12, + 0x41, 0x0a, 0x18, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x73, 0x69, 0x67, 0x6e, + 0x69, 0x6e, 0x67, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x72, 0x65, 0x66, 0x18, 0x12, 0x20, 0x01, 0x28, + 0x09, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0x18, 0x80, 0x01, 0x52, 0x15, 0x6d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x4b, 0x65, 0x79, 0x52, + 0x65, 0x66, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0xe9, + 0x0b, 0x0a, 0x0a, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x0e, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1d, 0x0a, + 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, + 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x5f, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x50, 0x72, 0x6f, 0x64, + 0x75, 0x63, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x5f, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x70, 0x72, + 0x6f, 0x64, 0x75, 0x63, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x55, 0x0a, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, + 0x72, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x31, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x41, + 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x52, 0x0c, 0x61, 0x72, 0x63, + 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x12, 0x3d, 0x0a, 0x04, 0x74, 0x79, 0x70, + 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x29, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, + 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x79, + 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x75, 0x70, 0x73, 0x74, + 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x75, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x55, 0x72, 0x6c, 0x12, 0x29, 0x0a, 0x10, 0x73, + 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4c, 0x6f, + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, + 0x68, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x75, 0x62, + 0x6c, 0x69, 0x73, 0x68, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x6c, 0x6c, 0x6f, 0x77, + 0x5f, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x61, + 0x6c, 0x6c, 0x6f, 0x77, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x29, 0x0a, 0x10, 0x69, 0x6e, + 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x18, 0x0d, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x50, 0x61, 0x63, + 0x6b, 0x61, 0x67, 0x65, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, + 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x18, 0x0e, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x0f, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, + 0x12, 0x4d, 0x0a, 0x0a, 0x67, 0x70, 0x67, 0x5f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x0f, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2e, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x47, 0x50, 0x47, 0x50, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x52, 0x09, 0x67, 0x70, 0x67, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, + 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x10, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x72, 0x65, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x61, + 0x63, 0x74, 0x69, 0x76, 0x65, 0x52, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, + 0x18, 0x0a, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x64, 0x41, 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, + 0x61, 0x74, 0x18, 0x15, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, + 0x35, 0x0a, 0x17, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, + 0x67, 0x65, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x16, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x14, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, + 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x39, 0x0a, 0x19, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, + 0x68, 0x5f, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x5f, 0x73, 0x6e, 0x61, 0x70, 0x73, + 0x68, 0x6f, 0x74, 0x18, 0x17, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x70, 0x75, 0x62, 0x6c, 0x69, + 0x73, 0x68, 0x42, 0x61, 0x73, 0x65, 0x55, 0x72, 0x6c, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, + 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x5f, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x18, 0x18, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, + 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x6c, 0x6f, 0x67, 0x69, 0x63, 0x61, + 0x6c, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x19, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x6c, + 0x6f, 0x67, 0x69, 0x63, 0x61, 0x6c, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x3c, 0x0a, 0x0c, 0x6c, + 0x61, 0x73, 0x74, 0x5f, 0x73, 0x79, 0x6e, 0x63, 0x5f, 0x61, 0x74, 0x18, 0x1a, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x6c, + 0x61, 0x73, 0x74, 0x53, 0x79, 0x6e, 0x63, 0x41, 0x74, 0x12, 0x59, 0x0a, 0x10, 0x6c, 0x61, 0x73, + 0x74, 0x5f, 0x73, 0x79, 0x6e, 0x63, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x1b, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x2f, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x52, 0x0e, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x12, 0x4a, 0x0a, 0x09, 0x73, 0x79, 0x6e, 0x63, 0x5f, 0x6d, 0x6f, 0x64, + 0x65, 0x18, 0x1c, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2d, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, + 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x79, + 0x6e, 0x63, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x08, 0x73, 0x79, 0x6e, 0x63, 0x4d, 0x6f, 0x64, 0x65, + 0x12, 0x32, 0x0a, 0x15, 0x62, 0x61, 0x6e, 0x64, 0x77, 0x69, 0x64, 0x74, 0x68, 0x5f, 0x6c, 0x69, + 0x6d, 0x69, 0x74, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x1d, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x13, 0x62, 0x61, 0x6e, 0x64, 0x77, 0x69, 0x64, 0x74, 0x68, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x42, + 0x79, 0x74, 0x65, 0x73, 0x12, 0x34, 0x0a, 0x16, 0x6d, 0x61, 0x78, 0x5f, 0x70, 0x61, 0x72, 0x61, + 0x6c, 0x6c, 0x65, 0x6c, 0x5f, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x18, 0x1e, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x14, 0x6d, 0x61, 0x78, 0x50, 0x61, 0x72, 0x61, 0x6c, 0x6c, 0x65, + 0x6c, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x12, 0x37, 0x0a, 0x18, 0x6d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x6b, + 0x65, 0x79, 0x5f, 0x72, 0x65, 0x66, 0x18, 0x1f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x6d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x4b, 0x65, 0x79, + 0x52, 0x65, 0x66, 0x12, 0x25, 0x0a, 0x0e, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x5f, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x20, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x72, 0x65, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x58, 0x0a, 0x17, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3d, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, + 0x73, 0x70, 0x65, 0x63, 0x22, 0x88, 0x01, 0x0a, 0x17, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x2e, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, + 0x18, 0x40, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, + 0x12, 0x3d, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, + 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, + 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x22, + 0x4b, 0x0a, 0x19, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x0d, + 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x0c, + 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, 0x22, 0xa5, 0x03, 0x0a, + 0x17, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x65, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, + 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xfa, 0x42, + 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, + 0x12, 0x2f, 0x0a, 0x0e, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x5f, 0x70, 0x72, 0x6f, 0x64, 0x75, + 0x63, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0x18, + 0x80, 0x01, 0x52, 0x0d, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, + 0x74, 0x12, 0x30, 0x0a, 0x0f, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x5f, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, + 0x02, 0x18, 0x40, 0x52, 0x0e, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x56, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x55, 0x0a, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, + 0x75, 0x72, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x31, 0x2e, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x41, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x52, 0x0c, 0x61, 0x72, + 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x12, 0x3d, 0x0a, 0x04, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x29, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, + 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x22, 0x0a, 0x07, 0x6b, 0x65, 0x79, + 0x77, 0x6f, 0x72, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, + 0x03, 0x18, 0x80, 0x01, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x45, 0x0a, + 0x0c, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x61, 0x67, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0b, 0x70, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x22, 0x70, 0x0a, 0x17, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x2e, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, + 0x40, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, 0x12, + 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x22, 0x87, 0x01, 0x0a, 0x18, 0x42, 0x61, 0x74, 0x63, 0x68, + 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, + 0x74, 0x65, 0x6d, 0x12, 0x2c, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, + 0x02, 0x18, 0x40, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, + 0x64, 0x12, 0x3d, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x29, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, + 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, + 0x22, 0x6a, 0x0a, 0x1d, 0x42, 0x61, 0x74, 0x63, 0x68, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x49, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x33, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x42, 0x61, 0x74, + 0x63, 0x68, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0xa2, 0x01, 0x0a, + 0x1a, 0x42, 0x61, 0x74, 0x63, 0x68, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x69, + 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x0a, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x1c, 0x0a, 0x09, + 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x45, 0x0a, 0x0a, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, + 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, + 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x22, 0x5b, 0x0a, 0x12, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x74, 0x72, + 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x22, 0xaf, + 0x01, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x69, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x0c, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x25, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x69, 0x65, 0x73, 0x12, 0x48, 0x0a, 0x0d, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x72, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x63, + 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x52, 0x0c, 0x70, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x71, 0x0a, 0x1e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, + 0x42, 0x61, 0x74, 0x63, 0x68, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x07, 0x72, 0x65, 0x73, 0x75, + 0x6c, 0x74, 0x73, 0x22, 0xb1, 0x04, 0x0a, 0x19, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x53, 0x70, 0x65, + 0x63, 0x12, 0x26, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x09, + 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x72, 0x05, 0x10, 0x01, + 0x18, 0x80, 0x01, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2a, 0x0a, 0x0b, 0x64, 0x65, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, + 0xfa, 0x42, 0x05, 0x72, 0x03, 0x18, 0x80, 0x08, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x5f, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x0a, 0xfa, 0x42, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0x80, 0x10, 0x52, 0x0e, 0x63, 0x6f, 0x6e, + 0x74, 0x72, 0x6f, 0x6c, 0x42, 0x61, 0x73, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x34, 0x0a, 0x10, 0x70, + 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x5f, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0x80, + 0x10, 0x52, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x42, 0x61, 0x73, 0x65, 0x55, 0x72, + 0x6c, 0x12, 0x30, 0x0a, 0x0f, 0x74, 0x6c, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, + 0x03, 0x18, 0xff, 0x01, 0x52, 0x0d, 0x74, 0x6c, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4e, + 0x61, 0x6d, 0x65, 0x12, 0x42, 0x0a, 0x17, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x65, 0x5f, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x40, 0x18, 0x5f, 0x52, + 0x16, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x46, 0x69, 0x6e, 0x67, + 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x88, 0x01, 0x01, 0x12, 0x58, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, + 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x40, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x53, 0x70, 0x65, 0x63, 0x2e, 0x4c, 0x61, 0x62, + 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, + 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 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, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f, + 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0xbb, 0x0d, 0x0a, 0x15, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, + 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, + 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, + 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x28, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x5f, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x42, 0x61, 0x73, 0x65, 0x55, 0x72, 0x6c, + 0x12, 0x28, 0x0a, 0x10, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x5f, 0x62, 0x61, 0x73, 0x65, + 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x70, 0x75, 0x62, 0x6c, + 0x69, 0x73, 0x68, 0x42, 0x61, 0x73, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x26, 0x0a, 0x0f, 0x74, 0x6c, + 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0d, 0x74, 0x6c, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4e, 0x61, + 0x6d, 0x65, 0x12, 0x37, 0x0a, 0x17, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x5f, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x16, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, + 0x46, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x12, 0x4e, 0x0a, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x36, 0x2e, 0x74, 0x72, + 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, + 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, + 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x27, 0x0a, 0x0f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, + 0x74, 0x65, 0x6d, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, + 0x66, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x12, 0x25, + 0x0a, 0x0e, 0x63, 0x61, 0x70, 0x61, 0x63, 0x69, 0x74, 0x79, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, + 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x63, 0x61, 0x70, 0x61, 0x63, 0x69, 0x74, 0x79, + 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x66, 0x72, 0x65, 0x65, 0x5f, 0x62, 0x79, + 0x74, 0x65, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x66, 0x72, 0x65, 0x65, 0x42, + 0x79, 0x74, 0x65, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x66, 0x72, 0x65, 0x65, 0x5f, 0x69, 0x6e, 0x6f, + 0x64, 0x65, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x66, 0x72, 0x65, 0x65, 0x49, + 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x12, 0x67, 0x0a, 0x0d, 0x74, 0x6f, 0x6f, 0x6c, 0x5f, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0f, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x42, 0x2e, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x2e, 0x54, + 0x6f, 0x6f, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x52, 0x0c, 0x74, 0x6f, 0x6f, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x54, + 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3c, + 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, + 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, + 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, + 0x62, 0x65, 0x6c, 0x73, 0x12, 0x46, 0x0a, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x68, 0x65, 0x61, + 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x5f, 0x61, 0x74, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0f, 0x6c, 0x61, 0x73, + 0x74, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x41, 0x74, 0x12, 0x3e, 0x0a, 0x0d, + 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x70, 0x72, 0x6f, 0x62, 0x65, 0x5f, 0x61, 0x74, 0x18, 0x12, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x0b, 0x6c, 0x61, 0x73, 0x74, 0x50, 0x72, 0x6f, 0x62, 0x65, 0x41, 0x74, 0x12, 0x26, 0x0a, 0x0f, + 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, + 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6c, 0x61, 0x73, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, + 0x43, 0x6f, 0x64, 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x10, 0x6c, 0x61, 0x73, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79, + 0x18, 0x15, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x42, + 0x79, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, + 0x16, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x39, 0x0a, 0x0a, + 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x17, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x75, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x18, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, + 0x73, 0x18, 0x19, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x69, 0x70, 0x41, 0x64, 0x64, 0x72, 0x65, + 0x73, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, + 0x72, 0x65, 0x18, 0x1a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, + 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x6f, 0x73, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x1b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x12, + 0x1d, 0x0a, 0x0a, 0x6f, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x1c, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x6f, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x23, + 0x0a, 0x0d, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, + 0x1d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x56, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x26, 0x0a, 0x0f, 0x64, 0x69, 0x73, 0x6b, 0x5f, 0x75, 0x73, 0x65, 0x64, + 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x64, 0x69, + 0x73, 0x6b, 0x55, 0x73, 0x65, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x64, + 0x69, 0x73, 0x6b, 0x5f, 0x75, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, + 0x74, 0x18, 0x1f, 0x20, 0x01, 0x28, 0x01, 0x52, 0x10, 0x64, 0x69, 0x73, 0x6b, 0x55, 0x73, 0x61, + 0x67, 0x65, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x20, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x43, + 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x3a, 0x0a, 0x19, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x5f, 0x70, 0x68, 0x79, 0x73, 0x69, 0x63, 0x61, 0x6c, 0x5f, 0x62, 0x79, 0x74, 0x65, + 0x73, 0x18, 0x21, 0x20, 0x01, 0x28, 0x03, 0x52, 0x17, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x50, 0x68, 0x79, 0x73, 0x69, 0x63, 0x61, 0x6c, 0x42, 0x79, 0x74, 0x65, 0x73, + 0x12, 0x28, 0x0a, 0x10, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x7a, 0x68, 0x5f, 0x6d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x18, 0x22, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x5a, 0x68, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x3d, 0x0a, 0x1b, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x5f, 0x7a, 0x68, 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, + 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x23, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x18, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5a, 0x68, 0x52, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, + 0x64, 0x65, 0x64, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x3f, 0x0a, 0x11, 0x54, 0x6f, 0x6f, + 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 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, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, + 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 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, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xaf, 0x08, 0x0a, 0x1f, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, 0x12, 0x12, + 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x5f, 0x70, 0x72, 0x6f, + 0x64, 0x75, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x79, 0x73, 0x74, + 0x65, 0x6d, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x70, 0x72, 0x6f, + 0x64, 0x75, 0x63, 0x74, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0e, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x12, 0x52, 0x0a, 0x0f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x29, 0x2e, 0x74, 0x72, + 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x54, 0x79, 0x70, 0x65, 0x12, 0x55, 0x0a, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, + 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x31, 0x2e, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x41, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x52, + 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x12, 0x21, 0x0a, + 0x0c, 0x75, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0b, 0x75, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x55, 0x72, 0x6c, + 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x6c, 0x6f, 0x67, 0x69, 0x63, 0x61, 0x6c, + 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x6c, 0x6f, + 0x67, 0x69, 0x63, 0x61, 0x6c, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x68, + 0x79, 0x73, 0x69, 0x63, 0x61, 0x6c, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0d, 0x70, 0x68, 0x79, 0x73, 0x69, 0x63, 0x61, 0x6c, 0x42, 0x79, 0x74, 0x65, + 0x73, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x72, 0x65, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x75, 0x72, + 0x72, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, + 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x0c, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x12, 0x3c, 0x0a, 0x0c, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x79, 0x6e, 0x63, + 0x5f, 0x61, 0x74, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x79, 0x6e, 0x63, 0x41, + 0x74, 0x12, 0x59, 0x0a, 0x10, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x79, 0x6e, 0x63, 0x5f, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2f, 0x2e, 0x74, 0x72, + 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0e, 0x6c, 0x61, + 0x73, 0x74, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x3c, 0x0a, 0x0c, + 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x73, 0x79, 0x6e, 0x63, 0x5f, 0x61, 0x74, 0x18, 0x0f, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, + 0x6e, 0x65, 0x78, 0x74, 0x53, 0x79, 0x6e, 0x63, 0x41, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x72, 0x75, + 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x10, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0d, 0x72, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x54, 0x61, 0x73, 0x6b, + 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x14, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x79, 0x6e, 0x63, 0x5f, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x79, 0x6e, 0x63, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x43, + 0x6f, 0x64, 0x65, 0x12, 0x35, 0x0a, 0x17, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x79, 0x6e, 0x63, + 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x12, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x79, 0x6e, 0x63, 0x45, 0x72, + 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x3a, 0x0a, 0x1a, 0x6c, 0x61, + 0x73, 0x74, 0x5f, 0x73, 0x79, 0x6e, 0x63, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x7a, 0x68, + 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, + 0x6c, 0x61, 0x73, 0x74, 0x53, 0x79, 0x6e, 0x63, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x5a, 0x68, 0x4d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x4f, 0x0a, 0x25, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, + 0x79, 0x6e, 0x63, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x7a, 0x68, 0x5f, 0x72, 0x65, 0x63, + 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x20, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x79, 0x6e, 0x63, 0x45, + 0x72, 0x72, 0x6f, 0x72, 0x5a, 0x68, 0x52, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x65, + 0x64, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x6e, 0x0a, 0x22, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, + 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x48, 0x0a, + 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x74, 0x72, + 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x53, 0x70, 0x65, + 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x22, 0xa1, 0x01, 0x0a, 0x22, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, + 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, + 0x0a, 0x0f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, + 0x18, 0x40, 0x52, 0x0d, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x49, + 0x64, 0x12, 0x48, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x34, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, + 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, + 0x65, 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x22, 0x59, 0x0a, 0x24, 0x44, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x0f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x6e, + 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, + 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x0d, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, + 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x22, 0x86, 0x02, 0x0a, 0x21, 0x4c, 0x69, 0x73, 0x74, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, + 0x4e, 0x6f, 0x64, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0a, + 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, + 0x42, 0x07, 0xfa, 0x42, 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, + 0x63, 0x74, 0x49, 0x64, 0x12, 0x4e, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x36, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, + 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x12, 0x22, 0x0a, 0x07, 0x6b, 0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0x18, 0x80, 0x01, 0x52, + 0x07, 0x6b, 0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x45, 0x0a, 0x0c, 0x70, 0x61, 0x67, 0x65, + 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, + 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, + 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x52, 0x0b, 0x70, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, + 0x7e, 0x0a, 0x22, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x0f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, + 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, + 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x0d, 0x73, 0x74, 0x6f, 0x72, 0x61, + 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x72, 0x6d, 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x22, + 0x56, 0x0a, 0x21, 0x50, 0x72, 0x6f, 0x62, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x0f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, + 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, + 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x0d, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, + 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x22, 0xc1, 0x02, 0x0a, 0x1d, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x0c, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x30, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, + 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, + 0x65, 0x52, 0x0b, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x5e, + 0x0a, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x65, 0x73, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, + 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x65, 0x73, 0x12, 0x6b, + 0x0a, 0x15, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x69, + 0x6e, 0x67, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x37, 0x2e, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x53, 0x69, 0x67, 0x6e, + 0x69, 0x6e, 0x67, 0x4b, 0x65, 0x79, 0x52, 0x13, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x4b, 0x65, 0x79, 0x73, 0x22, 0xf6, 0x02, 0x0a, 0x1c, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x4b, 0x65, 0x79, 0x12, 0x0e, 0x0a, 0x02, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x26, 0x0a, 0x0f, + 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, + 0x64, 0x65, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x6b, 0x65, 0x79, 0x5f, 0x72, 0x65, 0x66, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6b, 0x65, 0x79, 0x52, 0x65, 0x66, 0x12, 0x20, 0x0a, + 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x12, + 0x26, 0x0a, 0x0f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, + 0x4b, 0x65, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0c, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x55, 0x72, 0x6c, 0x12, 0x16, 0x0a, + 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x40, 0x0a, 0x0e, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0c, 0x6b, 0x65, 0x79, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x3b, 0x0a, 0x0b, 0x6f, 0x62, 0x73, 0x65, 0x72, + 0x76, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x64, 0x41, 0x74, 0x22, 0x92, 0x03, 0x0a, 0x22, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, + 0x64, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x0d, 0x73, + 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, + 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x0c, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, + 0x65, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x6e, 0x6f, 0x64, 0x65, + 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x4e, 0x6f, + 0x64, 0x65, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x6f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x6e, 0x6f, + 0x64, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x6f, 0x6e, 0x6c, 0x69, 0x6e, + 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x73, 0x12, 0x30, 0x0a, 0x14, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, + 0x63, 0x61, 0x70, 0x61, 0x63, 0x69, 0x74, 0x79, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x61, 0x70, 0x61, 0x63, + 0x69, 0x74, 0x79, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x13, 0x75, 0x73, 0x65, 0x64, + 0x5f, 0x63, 0x61, 0x70, 0x61, 0x63, 0x69, 0x74, 0x79, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x75, 0x73, 0x65, 0x64, 0x43, 0x61, 0x70, 0x61, 0x63, + 0x69, 0x74, 0x79, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x72, 0x69, 0x74, + 0x69, 0x63, 0x61, 0x6c, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0d, 0x63, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x4e, 0x6f, 0x64, 0x65, 0x73, 0x12, + 0x48, 0x0a, 0x0d, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, + 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0c, 0x70, 0x61, 0x67, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xfc, 0x04, 0x0a, 0x1c, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x45, 0x6e, 0x72, 0x6f, + 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x70, 0x65, 0x63, 0x12, 0x26, 0x0a, 0x0a, 0x70, 0x72, + 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, + 0xfa, 0x42, 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, + 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0x80, 0x01, 0x52, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x12, 0x2a, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0x18, 0x80, + 0x08, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, + 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x75, + 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x72, 0x05, 0x10, + 0x01, 0x18, 0x80, 0x10, 0x52, 0x0e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x42, 0x61, 0x73, + 0x65, 0x55, 0x72, 0x6c, 0x12, 0x34, 0x0a, 0x10, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x5f, + 0x62, 0x61, 0x73, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, + 0xfa, 0x42, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0x80, 0x10, 0x52, 0x0e, 0x70, 0x75, 0x62, 0x6c, + 0x69, 0x73, 0x68, 0x42, 0x61, 0x73, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x30, 0x0a, 0x0f, 0x74, 0x6c, + 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0x18, 0xff, 0x01, 0x52, 0x0d, 0x74, + 0x6c, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x5b, 0x0a, 0x06, + 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x43, 0x2e, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, + 0x74, 0x53, 0x70, 0x65, 0x63, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x66, 0x0a, 0x15, 0x65, 0x78, 0x70, + 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, + 0x72, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x31, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x41, + 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x52, 0x14, 0x65, 0x78, 0x70, + 0x65, 0x63, 0x74, 0x65, 0x64, 0x41, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, + 0x65, 0x12, 0x20, 0x0a, 0x0c, 0x74, 0x6d, 0x73, 0x5f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x69, + 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x74, 0x6d, 0x73, 0x41, 0x67, 0x65, 0x6e, + 0x74, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x0b, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x5f, + 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x18, + 0x40, 0x52, 0x0a, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x49, 0x64, 0x1a, 0x39, 0x0a, + 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 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, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x9e, 0x07, 0x0a, 0x18, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x45, 0x6e, 0x72, 0x6f, 0x6c, + 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x26, 0x0a, 0x0f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, + 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, + 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x1d, 0x0a, + 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x20, 0x0a, 0x0c, + 0x74, 0x6d, 0x73, 0x5f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x0a, 0x74, 0x6d, 0x73, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1e, + 0x0a, 0x0a, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x0a, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x51, + 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x39, + 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, + 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, + 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x5f, 0x74, 0x61, 0x73, + 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x69, 0x6e, 0x73, 0x74, + 0x61, 0x6c, 0x6c, 0x54, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x12, 0x66, 0x0a, 0x15, 0x65, 0x78, 0x70, + 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, + 0x72, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x31, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x41, + 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x52, 0x14, 0x65, 0x78, 0x70, + 0x65, 0x63, 0x74, 0x65, 0x64, 0x41, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, + 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61, 0x74, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x26, 0x0a, 0x0f, + 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, + 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6c, 0x61, 0x73, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, + 0x43, 0x6f, 0x64, 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x10, 0x6c, 0x61, 0x73, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79, + 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x42, + 0x79, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, + 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x39, 0x0a, 0x0a, + 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x75, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x1e, 0x0a, 0x0b, 0x74, 0x6d, 0x73, 0x5f, 0x74, + 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x6d, + 0x73, 0x54, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x12, 0x2c, 0x0a, 0x12, 0x61, 0x6c, 0x6c, 0x6f, 0x77, + 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x10, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x10, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x6c, 0x69, 0x73, 0x74, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, + 0x74, 0x5f, 0x69, 0x64, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x72, 0x74, 0x69, + 0x66, 0x61, 0x63, 0x74, 0x49, 0x64, 0x12, 0x29, 0x0a, 0x10, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, + 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x73, 0x72, 0x5f, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x18, + 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x73, 0x72, 0x53, 0x68, 0x61, 0x32, 0x35, 0x36, + 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x73, 0x73, 0x75, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x69, 0x73, 0x73, 0x75, 0x61, + 0x6e, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x74, 0x0a, 0x25, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, + 0x65, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x4b, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x37, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x45, 0x6e, 0x72, 0x6f, 0x6c, + 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x22, + 0xcb, 0x03, 0x0a, 0x20, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, + 0x64, 0x65, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x0c, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, + 0x6e, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x74, 0x72, 0x70, + 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x0b, 0x73, 0x74, + 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x53, 0x0a, 0x0a, 0x65, 0x6e, 0x72, + 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x33, 0x2e, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, + 0x6e, 0x74, 0x52, 0x0a, 0x65, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x3d, + 0x0a, 0x04, 0x74, 0x61, 0x73, 0x6b, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x04, 0x74, 0x61, 0x73, 0x6b, 0x12, 0x47, 0x0a, + 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2f, 0x2e, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x52, 0x06, + 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x62, 0x6f, 0x6f, 0x74, 0x73, 0x74, + 0x72, 0x61, 0x70, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0e, 0x62, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, + 0x4c, 0x0a, 0x14, 0x62, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x5f, 0x65, 0x78, 0x70, + 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x12, 0x62, 0x6f, 0x6f, 0x74, 0x73, + 0x74, 0x72, 0x61, 0x70, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x22, 0xe4, 0x04, + 0x0a, 0x1a, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, + 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x53, 0x70, 0x65, 0x63, 0x12, 0x1e, 0x0a, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x72, + 0x05, 0x10, 0x01, 0x18, 0x80, 0x01, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x07, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, + 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x12, 0x23, 0x0a, 0x07, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x20, 0x52, 0x07, 0x72, + 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, + 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x31, 0x2e, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x41, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x52, + 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x12, 0x27, 0x0a, + 0x09, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0xff, 0x01, 0x52, 0x08, 0x66, 0x69, + 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x26, 0x0a, 0x0a, 0x73, 0x69, 0x7a, 0x65, 0x5f, 0x62, + 0x79, 0x74, 0x65, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x22, + 0x02, 0x20, 0x00, 0x52, 0x09, 0x73, 0x69, 0x7a, 0x65, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x20, + 0x0a, 0x06, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, + 0xfa, 0x42, 0x05, 0x72, 0x03, 0x98, 0x01, 0x40, 0x52, 0x06, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, + 0x12, 0x2c, 0x0a, 0x0c, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, + 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x07, 0x18, + 0x40, 0x52, 0x0b, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x12, 0x2f, + 0x0a, 0x0d, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0x80, + 0x04, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, + 0x26, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x03, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x22, 0x02, 0x28, 0x00, 0x52, 0x09, 0x70, 0x72, + 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x39, 0x0a, 0x14, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, + 0x0b, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x18, 0x40, 0x52, 0x12, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x49, 0x64, 0x12, 0x28, 0x0a, 0x0b, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, + 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x18, 0x40, + 0x52, 0x0a, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x26, 0x0a, 0x0a, + 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x18, 0x40, 0x52, 0x09, 0x70, 0x61, 0x63, 0x6b, 0x61, + 0x67, 0x65, 0x49, 0x64, 0x22, 0xb9, 0x05, 0x0a, 0x16, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x12, + 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, + 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, + 0x07, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, + 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x31, 0x2e, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x41, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, + 0x52, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x12, 0x1b, + 0x0a, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, + 0x69, 0x7a, 0x65, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x09, 0x73, 0x69, 0x7a, 0x65, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x68, + 0x61, 0x32, 0x35, 0x36, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x68, 0x61, 0x32, + 0x35, 0x36, 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, + 0x69, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x43, + 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, + 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, + 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, + 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x1d, 0x0a, + 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x30, 0x0a, + 0x14, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, 0x12, + 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x10, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, + 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x11, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x49, 0x64, 0x12, + 0x14, 0x0a, 0x05, 0x65, 0x70, 0x6f, 0x63, 0x68, 0x18, 0x12, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, + 0x65, 0x70, 0x6f, 0x63, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x65, 0x76, 0x72, 0x61, 0x18, 0x13, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x65, 0x76, 0x72, 0x61, 0x12, 0x23, 0x0a, 0x0d, 0x69, + 0x6d, 0x6d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x14, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0c, 0x69, 0x6d, 0x6d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x72, 0x6c, + 0x22, 0x72, 0x0a, 0x25, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, + 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x49, 0x0a, 0x04, 0x73, 0x70, 0x65, + 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, + 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, + 0x64, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, + 0x73, 0x70, 0x65, 0x63, 0x22, 0x6f, 0x0a, 0x1e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, + 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, + 0x6f, 0x64, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x08, 0x61, 0x72, 0x74, + 0x69, 0x66, 0x61, 0x63, 0x74, 0x22, 0x6d, 0x0a, 0x22, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, + 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, + 0x72, 0x02, 0x18, 0x20, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x26, 0x0a, 0x0a, + 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, + 0x42, 0x07, 0xfa, 0x42, 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, + 0x63, 0x74, 0x49, 0x64, 0x22, 0x76, 0x0a, 0x23, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, + 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x09, 0x61, + 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x31, + 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, + 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, + 0x74, 0x52, 0x09, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x22, 0x53, 0x0a, 0x25, + 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x0b, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, + 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, + 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x0a, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x49, + 0x64, 0x22, 0xf1, 0x01, 0x0a, 0x1f, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x53, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x53, 0x70, 0x65, 0x63, 0x12, 0x26, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x22, 0x02, + 0x20, 0x00, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x2e, 0x0a, + 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x24, 0x18, 0x40, 0x52, + 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, 0x12, 0x55, 0x0a, + 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x31, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x41, 0x72, 0x63, 0x68, 0x69, 0x74, + 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x52, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, + 0x74, 0x75, 0x72, 0x65, 0x12, 0x1f, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x18, 0x20, 0x52, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x94, 0x03, 0x0a, 0x1b, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x53, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, + 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, + 0x63, 0x74, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, 0x12, 0x55, 0x0a, 0x0c, 0x61, 0x72, 0x63, + 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x31, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, + 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x41, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, + 0x72, 0x65, 0x52, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, + 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x64, 0x42, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x64, 0x5f, 0x62, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x64, 0x42, 0x79, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x64, 0x5f, 0x61, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, + 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0x7d, 0x0a, 0x2b, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x53, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x4e, 0x0a, 0x04, 0x73, + 0x70, 0x65, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x53, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x22, 0x84, 0x01, 0x0a, 0x23, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x6e, + 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x5d, 0x0a, 0x0e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x5f, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x74, 0x72, + 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x53, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x52, 0x0d, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x53, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x22, 0x51, 0x0a, 0x27, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x53, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, + 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x03, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, + 0x65, 0x63, 0x74, 0x49, 0x64, 0x22, 0x8b, 0x01, 0x0a, 0x28, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x73, 0x74, + 0x61, 0x6c, 0x6c, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x5f, 0x0a, 0x0f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x5f, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x74, 0x72, + 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x53, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x52, 0x0e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x53, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x73, 0x22, 0xa1, 0x03, 0x0a, 0x27, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x6c, 0x69, 0x73, 0x74, 0x53, 0x70, 0x65, 0x63, 0x12, + 0x26, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x03, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x09, 0x70, 0x72, + 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x29, 0x0a, 0x0c, 0x74, 0x6d, 0x73, 0x5f, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x42, 0x07, 0xfa, + 0x42, 0x04, 0x32, 0x02, 0x20, 0x00, 0x52, 0x0a, 0x74, 0x6d, 0x73, 0x41, 0x67, 0x65, 0x6e, 0x74, + 0x49, 0x64, 0x12, 0x35, 0x0a, 0x11, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x68, + 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, + 0x42, 0x05, 0x72, 0x03, 0x18, 0xff, 0x01, 0x52, 0x10, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, + 0x64, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x28, 0x0a, 0x0b, 0x65, 0x78, 0x70, + 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x69, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, + 0xfa, 0x42, 0x04, 0x72, 0x02, 0x18, 0x40, 0x52, 0x0a, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, + 0x64, 0x49, 0x70, 0x12, 0x66, 0x0a, 0x15, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, + 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x31, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x41, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, + 0x63, 0x74, 0x75, 0x72, 0x65, 0x52, 0x14, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x41, + 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x12, 0x1f, 0x0a, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, + 0x72, 0x02, 0x18, 0x20, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x39, 0x0a, 0x0a, + 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x65, 0x78, + 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x22, 0xb3, 0x04, 0x0a, 0x23, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x6c, 0x69, 0x73, 0x74, 0x12, + 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, + 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x20, + 0x0a, 0x0c, 0x74, 0x6d, 0x73, 0x5f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x74, 0x6d, 0x73, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x64, + 0x12, 0x2b, 0x0a, 0x11, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x68, 0x6f, 0x73, + 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x65, 0x78, 0x70, + 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, + 0x0b, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x69, 0x70, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x49, 0x70, 0x12, 0x66, + 0x0a, 0x15, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, + 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x31, 0x2e, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x41, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, + 0x52, 0x14, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x41, 0x72, 0x63, 0x68, 0x69, 0x74, + 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x39, + 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61, 0x74, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, + 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x42, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x64, 0x42, 0x79, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, + 0x41, 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, + 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0x8a, 0x01, + 0x0a, 0x30, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x6c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x56, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x42, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x6c, 0x69, 0x73, 0x74, + 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x22, 0x8b, 0x01, 0x0a, 0x2b, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x6c, 0x69, + 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5c, 0x0a, 0x09, 0x61, 0x6c, + 0x6c, 0x6f, 0x77, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3e, 0x2e, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x6c, 0x69, 0x73, 0x74, 0x52, 0x09, 0x61, + 0x6c, 0x6c, 0x6f, 0x77, 0x6c, 0x69, 0x73, 0x74, 0x22, 0x59, 0x0a, 0x2f, 0x4c, 0x69, 0x73, 0x74, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x6c, + 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0a, 0x70, + 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, + 0x07, 0xfa, 0x42, 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, + 0x74, 0x49, 0x64, 0x22, 0x92, 0x01, 0x0a, 0x30, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x6c, 0x69, 0x73, 0x74, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5e, 0x0a, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, + 0x77, 0x6c, 0x69, 0x73, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3e, 0x2e, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x6c, 0x69, 0x73, 0x74, 0x52, 0x0a, 0x61, 0x6c, + 0x6c, 0x6f, 0x77, 0x6c, 0x69, 0x73, 0x74, 0x73, 0x22, 0xe8, 0x06, 0x0a, 0x19, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x72, 0x65, 0x66, 0x6c, 0x69, 0x67, 0x68, 0x74, + 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x26, 0x0a, 0x0f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, + 0x65, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0d, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x23, + 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, + 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, + 0x65, 0x6d, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x66, + 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x12, 0x25, 0x0a, + 0x0e, 0x63, 0x61, 0x70, 0x61, 0x63, 0x69, 0x74, 0x79, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x63, 0x61, 0x70, 0x61, 0x63, 0x69, 0x74, 0x79, 0x42, + 0x79, 0x74, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x66, 0x72, 0x65, 0x65, 0x5f, 0x62, 0x79, 0x74, + 0x65, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x66, 0x72, 0x65, 0x65, 0x42, 0x79, + 0x74, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x13, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x5f, + 0x66, 0x72, 0x65, 0x65, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x11, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x46, 0x72, 0x65, 0x65, 0x42, 0x79, + 0x74, 0x65, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x66, 0x72, 0x65, 0x65, 0x5f, 0x69, 0x6e, 0x6f, 0x64, + 0x65, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x66, 0x72, 0x65, 0x65, 0x49, 0x6e, + 0x6f, 0x64, 0x65, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, + 0x5f, 0x69, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, + 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x49, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x12, 0x36, 0x0a, + 0x17, 0x65, 0x73, 0x74, 0x69, 0x6d, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x61, + 0x67, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x15, + 0x65, 0x73, 0x74, 0x69, 0x6d, 0x61, 0x74, 0x65, 0x64, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x34, 0x0a, 0x16, 0x65, 0x73, 0x74, 0x69, 0x6d, 0x61, 0x74, + 0x65, 0x64, 0x5f, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, + 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x14, 0x65, 0x73, 0x74, 0x69, 0x6d, 0x61, 0x74, 0x65, 0x64, + 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x6b, 0x0a, 0x0d, 0x74, + 0x6f, 0x6f, 0x6c, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0e, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x46, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x72, 0x65, 0x66, 0x6c, 0x69, 0x67, + 0x68, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x2e, 0x54, 0x6f, 0x6f, 0x6c, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0c, 0x74, 0x6f, 0x6f, 0x6c, + 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, + 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x39, 0x0a, 0x0a, + 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61, 0x74, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x65, 0x78, + 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, + 0x41, 0x74, 0x1a, 0x3f, 0x0a, 0x11, 0x54, 0x6f, 0x6f, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 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, 0x3a, + 0x02, 0x38, 0x01, 0x22, 0xcd, 0x01, 0x0a, 0x22, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x50, 0x72, 0x6f, + 0x62, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x0c, 0x73, 0x74, + 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x30, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, + 0x64, 0x65, 0x52, 0x0b, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x12, + 0x52, 0x0a, 0x09, 0x70, 0x72, 0x65, 0x66, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x72, 0x65, 0x66, 0x6c, 0x69, 0x67, + 0x68, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x09, 0x70, 0x72, 0x65, 0x66, 0x6c, 0x69, + 0x67, 0x68, 0x74, 0x22, 0xcf, 0x01, 0x0a, 0x25, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x72, 0x65, 0x66, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x52, + 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, + 0x0f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, + 0x40, 0x52, 0x0d, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x64, + 0x12, 0x2c, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x69, + 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x18, 0x40, + 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, 0x12, 0x45, + 0x0a, 0x0c, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x61, 0x67, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0b, 0x70, 0x61, 0x67, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xc2, 0x01, 0x0a, 0x26, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x72, 0x65, 0x66, 0x6c, 0x69, 0x67, 0x68, + 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x4e, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x34, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x72, 0x65, 0x66, 0x6c, 0x69, 0x67, 0x68, + 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, + 0x12, 0x48, 0x0a, 0x0d, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, + 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, + 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0c, 0x70, 0x61, + 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x47, 0x0a, 0x15, 0x53, 0x79, + 0x6e, 0x63, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, + 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x49, 0x64, 0x22, 0xbc, 0x02, 0x0a, 0x1a, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x53, 0x70, + 0x65, 0x63, 0x12, 0x52, 0x0a, 0x07, 0x63, 0x61, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x38, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x63, + 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x43, 0x61, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x07, 0x63, + 0x61, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x25, 0x0a, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, + 0x6e, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, + 0x01, 0x18, 0x40, 0x52, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x12, 0x29, 0x0a, + 0x0b, 0x64, 0x61, 0x79, 0x5f, 0x6f, 0x66, 0x5f, 0x77, 0x65, 0x65, 0x6b, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x05, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x1a, 0x04, 0x18, 0x06, 0x28, 0x00, 0x52, 0x09, 0x64, + 0x61, 0x79, 0x4f, 0x66, 0x57, 0x65, 0x65, 0x6b, 0x12, 0x1d, 0x0a, 0x04, 0x68, 0x6f, 0x75, 0x72, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x1a, 0x04, 0x18, 0x17, 0x28, + 0x00, 0x52, 0x04, 0x68, 0x6f, 0x75, 0x72, 0x12, 0x21, 0x0a, 0x06, 0x6d, 0x69, 0x6e, 0x75, 0x74, + 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x1a, 0x04, 0x18, 0x3b, + 0x28, 0x00, 0x52, 0x06, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 0x12, 0x36, 0x0a, 0x11, 0x66, 0x61, + 0x69, 0x6c, 0x75, 0x72, 0x65, 0x5f, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x05, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x1a, 0x04, 0x18, 0x64, 0x28, 0x01, + 0x52, 0x10, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, + 0x6c, 0x64, 0x22, 0xbc, 0x06, 0x0a, 0x16, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x0e, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x23, 0x0a, + 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, + 0x64, 0x12, 0x52, 0x0a, 0x07, 0x63, 0x61, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x38, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x63, 0x68, + 0x65, 0x64, 0x75, 0x6c, 0x65, 0x43, 0x61, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x07, 0x63, 0x61, + 0x64, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, + 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, + 0x65, 0x12, 0x1e, 0x0a, 0x0b, 0x64, 0x61, 0x79, 0x5f, 0x6f, 0x66, 0x5f, 0x77, 0x65, 0x65, 0x6b, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x64, 0x61, 0x79, 0x4f, 0x66, 0x57, 0x65, 0x65, + 0x6b, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x6f, 0x75, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x04, 0x68, 0x6f, 0x75, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 0x12, 0x4f, 0x0a, + 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x37, 0x2e, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x3a, + 0x0a, 0x0b, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x61, 0x74, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x09, 0x6c, 0x61, 0x73, 0x74, 0x52, 0x75, 0x6e, 0x41, 0x74, 0x12, 0x3a, 0x0a, 0x0b, 0x6e, 0x65, + 0x78, 0x74, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x61, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x6e, 0x65, 0x78, + 0x74, 0x52, 0x75, 0x6e, 0x41, 0x74, 0x12, 0x31, 0x0a, 0x14, 0x63, 0x6f, 0x6e, 0x73, 0x65, 0x63, + 0x75, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x73, 0x18, 0x0c, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x13, 0x63, 0x6f, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x74, 0x69, 0x76, + 0x65, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x66, 0x61, 0x69, + 0x6c, 0x75, 0x72, 0x65, 0x5f, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x0d, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x10, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x54, 0x68, 0x72, + 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x12, 0x26, 0x0a, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0d, 0x6c, 0x61, 0x73, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x2c, + 0x0a, 0x12, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x6c, 0x61, 0x73, 0x74, + 0x45, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1d, 0x0a, 0x0a, + 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x42, 0x79, 0x12, 0x39, 0x0a, 0x0a, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x64, 0x5f, 0x61, 0x74, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, + 0x74, 0x22, 0xa0, 0x01, 0x0a, 0x23, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, + 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x0d, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x0c, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, 0x12, 0x49, 0x0a, 0x04, 0x73, 0x70, 0x65, + 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, + 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x79, + 0x6e, 0x63, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, + 0x73, 0x70, 0x65, 0x63, 0x22, 0x57, 0x0a, 0x25, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x63, + 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, + 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, + 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, 0x22, 0x5b, 0x0a, + 0x29, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x0d, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x0c, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, 0x22, 0x6f, 0x0a, 0x1e, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x63, 0x68, 0x65, + 0x64, 0x75, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x08, + 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x31, + 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, + 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, + 0x65, 0x52, 0x08, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x22, 0x9c, 0x13, 0x0a, 0x0e, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x12, 0x0e, + 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x20, + 0x0a, 0x0c, 0x72, 0x6f, 0x6f, 0x74, 0x5f, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x6f, 0x6f, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x49, 0x64, + 0x12, 0x27, 0x0a, 0x10, 0x72, 0x65, 0x74, 0x72, 0x79, 0x5f, 0x6f, 0x66, 0x5f, 0x74, 0x61, 0x73, + 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x72, 0x65, 0x74, 0x72, + 0x79, 0x4f, 0x66, 0x54, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, 0x12, 0x1d, + 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x1f, 0x0a, + 0x0b, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x41, + 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2d, 0x2e, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, + 0x65, 0x12, 0x47, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x2f, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x34, 0x0a, 0x16, 0x72, 0x65, + 0x71, 0x75, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x6d, 0x75, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, + 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x72, 0x65, 0x71, 0x75, + 0x69, 0x72, 0x65, 0x73, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x6f, 0x63, 0x6b, + 0x12, 0x18, 0x0a, 0x07, 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x07, 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x61, + 0x78, 0x5f, 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x0b, 0x6d, 0x61, 0x78, 0x41, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x73, 0x12, 0x21, 0x0a, + 0x0c, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x18, 0x0c, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0b, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x42, 0x79, + 0x12, 0x1b, 0x0a, 0x09, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x0d, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49, 0x64, 0x12, 0x23, 0x0a, + 0x0d, 0x66, 0x65, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x0e, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x66, 0x65, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x12, 0x44, 0x0a, 0x10, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x5f, 0x65, 0x78, 0x70, 0x69, + 0x72, 0x65, 0x73, 0x5f, 0x61, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x45, + 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x46, 0x0a, 0x11, 0x6c, 0x61, 0x73, 0x74, + 0x5f, 0x68, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x5f, 0x61, 0x74, 0x18, 0x10, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x41, 0x74, + 0x12, 0x3d, 0x0a, 0x0c, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x61, 0x74, + 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x52, 0x0b, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x74, 0x12, + 0x39, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x12, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x3b, 0x0a, 0x0b, 0x66, 0x69, + 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x13, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x66, 0x69, 0x6e, + 0x69, 0x73, 0x68, 0x65, 0x64, 0x41, 0x74, 0x12, 0x4a, 0x0a, 0x13, 0x63, 0x61, 0x6e, 0x63, 0x65, + 0x6c, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x14, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x11, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, + 0x64, 0x41, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x5f, + 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x18, 0x15, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0f, 0x70, + 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x12, 0x21, + 0x0a, 0x0c, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x74, 0x65, 0x70, 0x18, 0x16, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x65, + 0x70, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, + 0x17, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, + 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x18, 0x18, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, + 0x5f, 0x61, 0x74, 0x18, 0x19, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, + 0x12, 0x39, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x1a, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x4c, 0x0a, 0x09, 0x64, + 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x69, 0x73, 0x18, 0x1b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, + 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, + 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x69, 0x73, 0x52, 0x09, + 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x69, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x1c, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0d, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x49, + 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x66, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x5f, 0x69, + 0x64, 0x18, 0x1d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x72, 0x65, 0x66, 0x6c, 0x69, 0x67, + 0x68, 0x74, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x5f, 0x73, + 0x74, 0x61, 0x74, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6d, 0x6d, + 0x69, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6d, 0x6d, 0x69, + 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x1f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, + 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x30, 0x0a, 0x14, 0x70, 0x72, + 0x65, 0x70, 0x61, 0x72, 0x65, 0x64, 0x5f, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x5f, + 0x69, 0x64, 0x18, 0x20, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x70, 0x72, 0x65, 0x70, 0x61, 0x72, + 0x65, 0x64, 0x52, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x38, 0x0a, 0x18, + 0x70, 0x72, 0x65, 0x70, 0x61, 0x72, 0x65, 0x64, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, + 0x74, 0x5f, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x18, 0x21, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, + 0x70, 0x72, 0x65, 0x70, 0x61, 0x72, 0x65, 0x64, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, + 0x53, 0x68, 0x61, 0x32, 0x35, 0x36, 0x12, 0x3b, 0x0a, 0x0b, 0x70, 0x72, 0x65, 0x70, 0x61, 0x72, + 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x22, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x70, 0x72, 0x65, 0x70, 0x61, 0x72, 0x65, + 0x64, 0x41, 0x74, 0x12, 0x3d, 0x0a, 0x0c, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x64, + 0x5f, 0x61, 0x74, 0x18, 0x23, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x64, + 0x41, 0x74, 0x12, 0x3d, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x65, 0x64, 0x5f, + 0x61, 0x74, 0x18, 0x24, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x65, 0x64, 0x41, + 0x74, 0x12, 0x28, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x5f, 0x6f, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x25, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x72, 0x65, 0x73, + 0x75, 0x6c, 0x74, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x30, 0x0a, 0x14, 0x72, + 0x65, 0x73, 0x75, 0x6c, 0x74, 0x5f, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x73, 0x68, 0x61, + 0x32, 0x35, 0x36, 0x18, 0x26, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x72, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x68, 0x61, 0x32, 0x35, 0x36, 0x12, 0x27, 0x0a, + 0x0f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x27, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, + 0x65, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x28, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x4e, 0x61, + 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x18, 0x29, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x12, 0x27, 0x0a, 0x0f, + 0x69, 0x64, 0x65, 0x6d, 0x70, 0x6f, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x6b, 0x65, 0x79, 0x18, + 0x2a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x69, 0x64, 0x65, 0x6d, 0x70, 0x6f, 0x74, 0x65, 0x6e, + 0x63, 0x79, 0x4b, 0x65, 0x79, 0x12, 0x32, 0x0a, 0x15, 0x62, 0x61, 0x6e, 0x64, 0x77, 0x69, 0x64, + 0x74, 0x68, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x2b, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x13, 0x62, 0x61, 0x6e, 0x64, 0x77, 0x69, 0x64, 0x74, 0x68, 0x4c, + 0x69, 0x6d, 0x69, 0x74, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x34, 0x0a, 0x16, 0x6d, 0x61, 0x78, + 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6c, 0x6c, 0x65, 0x6c, 0x5f, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, + 0x61, 0x64, 0x73, 0x18, 0x2c, 0x20, 0x01, 0x28, 0x05, 0x52, 0x14, 0x6d, 0x61, 0x78, 0x50, 0x61, + 0x72, 0x61, 0x6c, 0x6c, 0x65, 0x6c, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x12, + 0x1d, 0x0a, 0x0a, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x2d, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x19, + 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x2e, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x78, 0x65, + 0x63, 0x75, 0x74, 0x6f, 0x72, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x2f, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x49, + 0x0a, 0x07, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x18, 0x30, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x2f, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, + 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, + 0x52, 0x07, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x12, 0x28, 0x0a, 0x10, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x5f, 0x7a, 0x68, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x31, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5a, 0x68, 0x4d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x12, 0x3d, 0x0a, 0x1b, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x7a, 0x68, 0x5f, + 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x32, 0x20, 0x01, 0x28, 0x09, 0x52, 0x18, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5a, + 0x68, 0x52, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x41, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x56, 0x0a, 0x0c, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x5f, 0x69, 0x74, 0x65, + 0x6d, 0x73, 0x18, 0x33, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, + 0x61, 0x73, 0x6b, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x0b, 0x72, + 0x65, 0x73, 0x75, 0x6c, 0x74, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x22, 0xd9, 0x03, 0x0a, 0x18, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x73, + 0x75, 0x6c, 0x74, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x61, 0x73, 0x6b, 0x5f, + 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, + 0x12, 0x19, 0x0a, 0x08, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x69, 0x74, 0x65, 0x6d, 0x4b, 0x65, 0x79, 0x12, 0x23, 0x0a, 0x0d, 0x70, + 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x65, 0x76, 0x72, 0x61, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0c, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x4e, 0x65, 0x76, 0x72, 0x61, + 0x12, 0x47, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x2f, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x5f, 0x6a, 0x73, 0x6f, + 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x4a, + 0x73, 0x6f, 0x6e, 0x12, 0x39, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x61, + 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x3b, + 0x0a, 0x0b, 0x66, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x0a, 0x66, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x41, 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0xf7, 0x05, 0x0a, 0x14, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, + 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, + 0x17, 0x0a, 0x07, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x61, 0x72, 0x67, + 0x65, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, + 0x61, 0x72, 0x67, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x61, 0x72, + 0x67, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x61, + 0x72, 0x67, 0x65, 0x74, 0x49, 0x64, 0x12, 0x26, 0x0a, 0x0f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, + 0x65, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0d, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x20, + 0x0a, 0x0c, 0x74, 0x6d, 0x73, 0x5f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x74, 0x6d, 0x73, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x64, + 0x12, 0x47, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x2f, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x70, 0x72, 0x6f, + 0x67, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x0f, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x50, 0x65, 0x72, + 0x63, 0x65, 0x6e, 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x5f, + 0x61, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, + 0x3b, 0x0a, 0x0b, 0x66, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x0a, 0x66, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x41, 0x74, 0x12, 0x1d, 0x0a, 0x0a, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x0c, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x5f, 0x6a, 0x73, 0x6f, 0x6e, 0x18, + 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x4a, 0x73, 0x6f, + 0x6e, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, + 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x39, 0x0a, 0x0a, + 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x75, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x65, 0x72, 0x72, 0x6f, 0x72, + 0x5f, 0x7a, 0x68, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x10, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5a, 0x68, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x12, 0x3d, 0x0a, 0x1b, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x7a, 0x68, 0x5f, 0x72, 0x65, + 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x18, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5a, 0x68, 0x52, + 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x22, 0x43, 0x0a, 0x1d, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x22, 0x0a, 0x07, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x06, 0x74, + 0x61, 0x73, 0x6b, 0x49, 0x64, 0x22, 0x9d, 0x04, 0x0a, 0x1a, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, + 0x72, 0x02, 0x18, 0x40, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x49, 0x64, 0x12, 0x47, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x2f, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x26, 0x0a, 0x0a, 0x70, + 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x42, + 0x07, 0xfa, 0x42, 0x04, 0x22, 0x02, 0x28, 0x00, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, + 0x74, 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x0f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x6e, + 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, + 0x04, 0x72, 0x02, 0x18, 0x40, 0x52, 0x0d, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, + 0x64, 0x65, 0x49, 0x64, 0x12, 0x41, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x2d, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x54, 0x79, 0x70, + 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x21, 0x0a, 0x07, 0x74, 0x72, 0x69, 0x67, 0x67, + 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x18, + 0x20, 0x52, 0x07, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x12, 0x42, 0x0a, 0x0f, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x46, 0x72, 0x6f, 0x6d, 0x12, 0x3e, + 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x74, 0x6f, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x52, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x54, 0x6f, 0x12, 0x45, + 0x0a, 0x0c, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x61, 0x67, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0b, 0x70, 0x61, 0x67, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xac, 0x01, 0x0a, 0x18, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x53, 0x74, 0x61, 0x74, 0x69, 0x73, 0x74, 0x69, + 0x63, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x70, 0x65, 0x6e, 0x64, 0x69, + 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x75, 0x6e, 0x6e, 0x69, + 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, + 0x72, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x0c, + 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0b, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, + 0x23, 0x0a, 0x0d, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x43, + 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x41, 0x0a, 0x1b, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x07, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, + 0x06, 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x22, 0x40, 0x0a, 0x1a, 0x52, 0x65, 0x74, 0x72, 0x79, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x07, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, + 0x40, 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x22, 0x57, 0x0a, 0x16, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x04, 0x74, 0x61, 0x73, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x29, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x04, 0x74, 0x61, + 0x73, 0x6b, 0x22, 0xb7, 0x05, 0x0a, 0x15, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x4f, 0x43, 0x41, 0x49, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x17, 0x0a, 0x07, + 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, + 0x61, 0x73, 0x6b, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, 0x12, 0x27, 0x0a, 0x0f, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, + 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x5f, 0x70, 0x72, + 0x6f, 0x64, 0x75, 0x63, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x79, 0x73, + 0x74, 0x65, 0x6d, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x70, 0x72, + 0x6f, 0x64, 0x75, 0x63, 0x74, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0e, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x56, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, + 0x75, 0x72, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, + 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x61, 0x73, 0x6b, 0x5f, + 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x61, 0x73, 0x6b, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x61, 0x73, 0x6b, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, + 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, + 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x1f, 0x0a, + 0x0b, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x21, + 0x0a, 0x0c, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x74, 0x65, 0x70, 0x18, 0x0b, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x65, + 0x70, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, + 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, + 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x4c, 0x0a, 0x09, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, + 0x69, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x44, + 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x69, 0x73, 0x52, 0x09, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, + 0x73, 0x69, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x6c, 0x6f, 0x67, 0x5f, 0x73, 0x75, 0x6d, 0x6d, 0x61, + 0x72, 0x79, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6c, 0x6f, 0x67, 0x53, 0x75, 0x6d, + 0x6d, 0x61, 0x72, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x5f, + 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, + 0x65, 0x78, 0x74, 0x4a, 0x73, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x6e, 0x74, 0x65, + 0x78, 0x74, 0x5f, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x53, 0x68, 0x61, 0x32, 0x35, 0x36, 0x12, 0x1c, + 0x0a, 0x09, 0x74, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x65, 0x64, 0x18, 0x12, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x09, 0x74, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x65, 0x64, 0x22, 0x85, 0x03, 0x0a, + 0x16, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x43, 0x41, 0x49, 0x41, + 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x61, 0x73, 0x6b, 0x5f, + 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, + 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, + 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x5f, 0x73, 0x68, 0x61, 0x32, 0x35, + 0x36, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, + 0x53, 0x68, 0x61, 0x32, 0x35, 0x36, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x24, + 0x0a, 0x0e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x61, + 0x73, 0x6b, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, + 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x72, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x42, 0x79, 0x12, 0x39, 0x0a, 0x0a, + 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x3b, 0x0a, 0x0b, 0x66, 0x65, 0x65, 0x64, 0x62, + 0x61, 0x63, 0x6b, 0x5f, 0x61, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x66, 0x65, 0x65, 0x64, 0x62, 0x61, + 0x63, 0x6b, 0x41, 0x74, 0x22, 0x47, 0x0a, 0x21, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x43, 0x41, 0x49, 0x43, 0x6f, 0x6e, 0x74, 0x65, + 0x78, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x07, 0x74, 0x61, 0x73, + 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, + 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x22, 0x49, 0x0a, + 0x23, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x4f, 0x43, 0x41, 0x49, 0x41, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x07, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, + 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x22, 0x53, 0x0a, 0x25, 0x44, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x43, + 0x41, 0x49, 0x41, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x2a, 0x0a, 0x0b, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, 0x73, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, + 0x40, 0x52, 0x0a, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, 0x73, 0x49, 0x64, 0x22, 0xb0, 0x01, + 0x0a, 0x23, 0x41, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x4f, 0x43, 0x41, 0x49, 0x46, 0x65, 0x65, 0x64, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, + 0x10, 0x01, 0x18, 0x40, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, + 0x2f, 0x0a, 0x0e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, + 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, + 0x18, 0x40, 0x52, 0x0c, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x61, 0x73, 0x6b, 0x49, 0x64, + 0x12, 0x2e, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, + 0x18, 0x20, 0x52, 0x0c, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, + 0x22, 0x6b, 0x0a, 0x1d, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x43, + 0x41, 0x49, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x4a, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x43, 0x41, 0x49, 0x43, 0x6f, 0x6e, + 0x74, 0x65, 0x78, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0xdd, 0x01, + 0x0a, 0x1e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x43, 0x41, 0x49, + 0x41, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x4d, 0x0a, 0x08, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, 0x73, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x43, 0x41, 0x49, 0x41, 0x6e, 0x61, + 0x6c, 0x79, 0x73, 0x69, 0x73, 0x52, 0x08, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, 0x73, 0x12, + 0x4a, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x30, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x43, 0x41, 0x49, 0x43, 0x6f, 0x6e, 0x74, 0x65, + 0x78, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x69, + 0x6e, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x80, 0x01, + 0x0a, 0x1f, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x2e, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, + 0x01, 0x18, 0x40, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, + 0x64, 0x12, 0x2d, 0x0a, 0x12, 0x69, 0x6d, 0x6d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x72, + 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x69, + 0x6d, 0x6d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x22, 0xc0, 0x03, 0x0a, 0x10, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, 0x12, 0x2c, 0x0a, 0x12, 0x61, 0x63, + 0x74, 0x69, 0x76, 0x65, 0x5f, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x52, 0x65, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, + 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, + 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x21, + 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, + 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x62, + 0x61, 0x73, 0x65, 0x75, 0x72, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x61, + 0x73, 0x65, 0x75, 0x72, 0x6c, 0x12, 0x21, 0x0a, 0x0c, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x72, 0x65, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x55, 0x72, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x67, 0x70, 0x67, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x67, 0x70, 0x67, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x23, + 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x67, 0x70, 0x67, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x18, + 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x47, 0x70, 0x67, 0x63, 0x68, + 0x65, 0x63, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x67, 0x70, 0x67, 0x6b, 0x65, 0x79, 0x5f, 0x75, 0x72, + 0x6c, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x67, 0x70, 0x67, 0x6b, 0x65, 0x79, + 0x55, 0x72, 0x6c, 0x73, 0x12, 0x2d, 0x0a, 0x12, 0x69, 0x6d, 0x6d, 0x75, 0x74, 0x61, 0x62, 0x6c, + 0x65, 0x5f, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x11, 0x69, 0x6d, 0x6d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x22, 0x5f, 0x0a, 0x18, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x43, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x2b, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, + 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x22, 0x89, 0x02, 0x0a, 0x10, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x47, 0x50, 0x47, 0x4b, 0x65, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, 0x12, 0x15, + 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x6b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, + 0x72, 0x69, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x66, 0x69, 0x6e, 0x67, + 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x70, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0d, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x50, 0x61, 0x74, 0x68, 0x12, + 0x24, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x75, 0x72, + 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, + 0x65, 0x79, 0x55, 0x72, 0x6c, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, + 0x5f, 0x61, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, + 0x22, 0x70, 0x0a, 0x1f, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x47, 0x50, 0x47, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, + 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, + 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, + 0x65, 0x79, 0x22, 0x4e, 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x47, 0x50, 0x47, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, + 0x10, 0x01, 0x18, 0x40, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x49, 0x64, 0x22, 0x98, 0x01, 0x0a, 0x1d, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x47, 0x50, 0x47, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, + 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x49, 0x64, 0x12, 0x20, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, + 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, + 0x6d, 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x22, 0x59, 0x0a, + 0x18, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x47, 0x50, 0x47, 0x4b, 0x65, + 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x47, 0x50, 0x47, + 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x60, 0x0a, 0x1d, 0x4c, 0x69, 0x73, 0x74, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x47, 0x50, 0x47, 0x4b, 0x65, 0x79, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x04, 0x6b, 0x65, 0x79, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, + 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x47, 0x50, + 0x47, 0x4b, 0x65, 0x79, 0x52, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x22, 0xfd, 0x01, 0x0a, 0x1b, 0x4c, + 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, + 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x05, 0x74, 0x61, + 0x73, 0x6b, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x54, 0x61, 0x73, 0x6b, 0x52, 0x05, 0x74, 0x61, 0x73, 0x6b, 0x73, 0x12, 0x53, 0x0a, 0x0a, 0x73, + 0x74, 0x61, 0x74, 0x69, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x33, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, + 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x53, 0x74, 0x61, 0x74, 0x69, 0x73, + 0x74, 0x69, 0x63, 0x73, 0x52, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x69, 0x73, 0x74, 0x69, 0x63, 0x73, + 0x12, 0x48, 0x0a, 0x0d, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, + 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, + 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0c, 0x70, 0x61, + 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x8d, 0x07, 0x0a, 0x10, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x12, + 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, + 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, + 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, + 0x74, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, + 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e, + 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x23, 0x0a, + 0x0d, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x69, + 0x7a, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x64, 0x5f, 0x73, + 0x69, 0x7a, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x72, 0x65, 0x63, 0x65, 0x69, + 0x76, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x65, 0x78, 0x70, 0x65, 0x63, + 0x74, 0x65, 0x64, 0x5f, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0e, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x68, 0x61, 0x32, 0x35, 0x36, + 0x12, 0x23, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x75, 0x61, 0x6c, 0x5f, 0x73, 0x68, 0x61, 0x32, 0x35, + 0x36, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x63, 0x74, 0x75, 0x61, 0x6c, 0x53, + 0x68, 0x61, 0x32, 0x35, 0x36, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, + 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1d, 0x0a, + 0x0a, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x23, 0x0a, 0x0d, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x0d, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61, 0x74, 0x18, + 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x3d, 0x0a, 0x0c, + 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0f, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0b, + 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x64, 0x5f, 0x61, 0x74, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, + 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x6f, 0x64, + 0x65, 0x5f, 0x69, 0x64, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x74, 0x6f, 0x72, + 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x2d, 0x0a, 0x12, 0x63, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x5f, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x18, + 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x46, 0x69, 0x6e, + 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x68, 0x75, 0x6e, + 0x6b, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x63, 0x68, + 0x75, 0x6e, 0x6b, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x6f, 0x74, 0x61, 0x6c, + 0x5f, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x73, 0x18, 0x15, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x74, + 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x75, 0x70, + 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x16, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x0d, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x12, 0x37, 0x0a, 0x09, 0x70, 0x61, 0x75, 0x73, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x17, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x08, 0x70, 0x61, 0x75, 0x73, 0x65, 0x64, 0x41, 0x74, 0x22, 0xb1, 0x02, 0x0a, 0x1c, 0x42, + 0x65, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, + 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x0d, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x0c, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, 0x12, 0x27, 0x0a, 0x09, 0x66, + 0x69, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, + 0xfa, 0x42, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0xff, 0x01, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, + 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x26, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x73, 0x69, + 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x22, 0x02, 0x20, + 0x00, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x30, 0x0a, 0x0f, + 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x18, 0x40, 0x52, 0x0e, + 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x68, 0x61, 0x32, 0x35, 0x36, 0x12, 0x36, + 0x0a, 0x12, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, + 0x72, 0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, + 0x02, 0x18, 0x40, 0x52, 0x11, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x46, 0x69, 0x6e, 0x67, 0x65, + 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x0a, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x5f, + 0x73, 0x69, 0x7a, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x1a, + 0x02, 0x28, 0x00, 0x52, 0x09, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x53, 0x69, 0x7a, 0x65, 0x22, 0x81, + 0x01, 0x0a, 0x22, 0x41, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x09, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, + 0x01, 0x18, 0x40, 0x52, 0x08, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x49, 0x64, 0x12, 0x1f, 0x0a, + 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xfa, + 0x42, 0x04, 0x22, 0x02, 0x28, 0x00, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x12, + 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, + 0x74, 0x61, 0x22, 0xb4, 0x01, 0x0a, 0x1f, 0x50, 0x75, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x09, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, + 0x10, 0x01, 0x18, 0x40, 0x52, 0x08, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x49, 0x64, 0x12, 0x28, + 0x0a, 0x0b, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x05, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x1a, 0x02, 0x28, 0x00, 0x52, 0x0a, 0x63, 0x68, + 0x75, 0x6e, 0x6b, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x2b, 0x0a, 0x0c, 0x63, 0x68, 0x75, 0x6e, + 0x6b, 0x5f, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, + 0xfa, 0x42, 0x05, 0x72, 0x03, 0x98, 0x01, 0x40, 0x52, 0x0b, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x53, + 0x68, 0x61, 0x32, 0x35, 0x36, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0xaa, 0x01, 0x0a, 0x15, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x43, 0x68, + 0x75, 0x6e, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x5f, 0x69, 0x6e, 0x64, + 0x65, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x49, + 0x6e, 0x64, 0x65, 0x78, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x69, 0x7a, 0x65, 0x5f, 0x62, 0x79, 0x74, + 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x73, 0x69, 0x7a, 0x65, 0x42, 0x79, + 0x74, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x12, 0x39, 0x0a, 0x0a, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0xaf, 0x01, 0x0a, 0x20, 0x50, 0x75, 0x74, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x43, 0x68, + 0x75, 0x6e, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x75, + 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x74, 0x72, + 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x06, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, + 0x12, 0x46, 0x0a, 0x05, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x30, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, + 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x43, 0x68, 0x75, 0x6e, + 0x6b, 0x52, 0x05, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x22, 0xeb, 0x01, 0x0a, 0x26, 0x44, 0x65, 0x73, + 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, + 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, + 0x52, 0x06, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x48, 0x0a, 0x06, 0x63, 0x68, 0x75, 0x6e, + 0x6b, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, + 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x52, 0x06, 0x63, 0x68, 0x75, 0x6e, + 0x6b, 0x73, 0x12, 0x32, 0x0a, 0x15, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x68, + 0x75, 0x6e, 0x6b, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, + 0x05, 0x52, 0x13, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x49, + 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x22, 0x49, 0x0a, 0x1f, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, + 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, + 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x09, 0x75, 0x70, 0x6c, + 0x6f, 0x61, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, + 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x08, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x49, + 0x64, 0x22, 0x46, 0x0a, 0x1c, 0x41, 0x62, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x26, 0x0a, 0x09, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, + 0x08, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x49, 0x64, 0x22, 0x49, 0x0a, 0x1f, 0x44, 0x65, 0x73, + 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, + 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x09, + 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x08, 0x75, 0x70, 0x6c, 0x6f, + 0x61, 0x64, 0x49, 0x64, 0x22, 0x5f, 0x0a, 0x18, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x43, 0x0a, 0x06, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x2b, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x06, 0x75, + 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0xa6, 0x01, 0x0a, 0x20, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, + 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, + 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x75, 0x70, + 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x74, 0x72, 0x70, + 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x06, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x12, + 0x3d, 0x0a, 0x04, 0x74, 0x61, 0x73, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x04, 0x74, 0x61, 0x73, 0x6b, 0x22, 0xa4, + 0x04, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, 0x63, + 0x6b, 0x61, 0x67, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x02, 0x69, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, + 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, + 0x0a, 0x05, 0x65, 0x70, 0x6f, 0x63, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, + 0x70, 0x6f, 0x63, 0x68, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x18, + 0x0a, 0x07, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x61, 0x72, 0x63, 0x68, + 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, + 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x6e, 0x65, 0x76, 0x72, 0x61, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x65, 0x76, + 0x72, 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, + 0x1d, 0x0a, 0x0a, 0x73, 0x69, 0x7a, 0x65, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x0b, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x09, 0x73, 0x69, 0x7a, 0x65, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x16, + 0x0a, 0x06, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x39, + 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0e, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, + 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x73, 0x69, 0x67, + 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x0f, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x12, 0x22, 0x0a, 0x0d, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x5f, 0x6b, + 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x69, 0x67, + 0x6e, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x2b, 0x0a, 0x11, 0x73, 0x69, 0x67, 0x6e, + 0x61, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x11, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x10, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0xb5, 0x02, 0x0a, 0x1d, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, + 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x0b, 0x72, 0x65, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, + 0x04, 0x72, 0x02, 0x18, 0x40, 0x52, 0x0a, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x49, + 0x64, 0x12, 0x1c, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0x18, 0xff, 0x01, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, + 0x55, 0x0a, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x31, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x41, 0x72, 0x63, 0x68, + 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x52, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, + 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x12, 0x45, 0x0a, 0x0c, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x72, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x63, 0x6f, + 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x52, 0x0b, 0x70, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xd5, 0x01, + 0x0a, 0x1e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x49, + 0x64, 0x12, 0x48, 0x0a, 0x08, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x18, 0x02, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, + 0x65, 0x52, 0x08, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x12, 0x48, 0x0a, 0x0d, 0x70, + 0x61, 0x67, 0x65, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0c, 0x70, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x8b, 0x03, 0x0a, 0x18, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x51, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x69, 0x6e, 0x65, 0x49, 0x74, + 0x65, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, + 0x69, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x61, 0x73, 0x6b, 0x5f, + 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, + 0x12, 0x1b, 0x0a, 0x09, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x49, 0x64, 0x12, 0x23, 0x0a, + 0x0d, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x4e, 0x61, + 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x69, 0x7a, 0x65, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x73, 0x69, 0x7a, 0x65, 0x42, 0x79, 0x74, 0x65, + 0x73, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x61, + 0x73, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, + 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x72, 0x65, + 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x09, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0d, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x6e, 0x65, + 0x76, 0x72, 0x61, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x65, 0x74, 0x65, 0x63, + 0x74, 0x65, 0x64, 0x4e, 0x65, 0x76, 0x72, 0x61, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x64, 0x41, 0x74, 0x22, 0xe4, 0x01, 0x0a, 0x1f, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x51, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x69, 0x6e, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, + 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, 0x12, 0x20, 0x0a, 0x07, 0x74, 0x61, 0x73, 0x6b, 0x5f, + 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x18, + 0x40, 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x0b, 0x72, 0x65, 0x61, + 0x73, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, + 0xfa, 0x42, 0x04, 0x72, 0x02, 0x18, 0x40, 0x52, 0x0a, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x43, + 0x6f, 0x64, 0x65, 0x12, 0x45, 0x0a, 0x0c, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x72, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, + 0x6e, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0b, 0x70, + 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xb7, 0x01, 0x0a, 0x20, 0x4c, + 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x51, 0x75, 0x61, + 0x72, 0x61, 0x6e, 0x74, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x49, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, + 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, + 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x51, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x69, 0x6e, 0x65, 0x49, + 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x48, 0x0a, 0x0d, 0x70, 0x61, + 0x67, 0x65, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x23, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0c, 0x70, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x72, 0x0a, 0x1e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, + 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, 0x12, 0x20, 0x0a, 0x05, 0x6e, 0x65, 0x76, 0x72, 0x61, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0x80, + 0x04, 0x52, 0x05, 0x6e, 0x65, 0x76, 0x72, 0x61, 0x22, 0xc6, 0x02, 0x0a, 0x13, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x69, 0x73, + 0x12, 0x29, 0x0a, 0x10, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x5f, 0x63, 0x61, 0x74, 0x65, + 0x67, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x66, 0x61, 0x69, 0x6c, + 0x75, 0x72, 0x65, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x66, 0x61, + 0x69, 0x6c, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x65, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0a, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x53, 0x74, 0x65, 0x70, 0x12, 0x21, 0x0a, 0x0c, 0x6c, + 0x69, 0x6b, 0x65, 0x6c, 0x79, 0x5f, 0x63, 0x61, 0x75, 0x73, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0b, 0x6c, 0x69, 0x6b, 0x65, 0x6c, 0x79, 0x43, 0x61, 0x75, 0x73, 0x65, 0x12, 0x1a, + 0x0a, 0x08, 0x65, 0x76, 0x69, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x08, 0x65, 0x76, 0x69, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x2d, 0x0a, 0x12, 0x72, 0x65, + 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, + 0x64, 0x65, 0x64, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x75, 0x6c, + 0x65, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x75, 0x6c, 0x65, + 0x49, 0x64, 0x12, 0x3d, 0x0a, 0x0c, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x5f, + 0x61, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0b, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x41, + 0x74, 0x22, 0x4a, 0x0a, 0x24, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, + 0x69, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x07, 0x74, 0x61, 0x73, + 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, + 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x22, 0x95, 0x01, + 0x0a, 0x21, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x44, 0x69, 0x61, 0x67, + 0x6e, 0x6f, 0x73, 0x69, 0x73, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, + 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, + 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x12, 0x18, 0x0a, 0x07, 0x63, + 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, + 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x22, 0xd0, 0x01, 0x0a, 0x1a, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, + 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, + 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x70, 0x12, + 0x21, 0x0a, 0x0c, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x73, 0x73, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, + 0x12, 0x46, 0x0a, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x68, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, + 0x61, 0x74, 0x5f, 0x61, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x48, 0x65, 0x61, + 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x41, 0x74, 0x22, 0x28, 0x0a, 0x26, 0x4c, 0x69, 0x73, 0x74, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x6f, 0x64, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x22, 0x76, 0x0a, 0x27, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, + 0x4e, 0x6f, 0x64, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, + 0x05, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x4e, + 0x6f, 0x64, 0x65, 0x52, 0x05, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x22, 0xe1, 0x01, 0x0a, 0x23, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x44, + 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, + 0x10, 0x01, 0x18, 0x40, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x49, 0x64, 0x12, 0x28, 0x0a, 0x0b, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, + 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x18, 0x40, + 0x52, 0x0a, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x2b, 0x0a, 0x0a, + 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x69, 0x70, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, + 0x42, 0x0c, 0xfa, 0x42, 0x09, 0x92, 0x01, 0x06, 0x08, 0x01, 0x10, 0x64, 0x18, 0x01, 0x52, 0x09, + 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x49, 0x70, 0x73, 0x12, 0x33, 0x0a, 0x0f, 0x74, 0x69, 0x6d, + 0x65, 0x6f, 0x75, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x05, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x1a, 0x05, 0x18, 0x88, 0x0e, 0x28, 0x00, 0x52, 0x0e, + 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x22, 0x5b, + 0x0a, 0x25, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x0f, 0x64, 0x69, 0x73, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x0e, 0x64, 0x69, 0x73, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0x9b, 0x01, 0x0a, 0x22, + 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x44, 0x69, + 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, + 0x10, 0x01, 0x18, 0x40, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x49, 0x64, 0x12, 0x45, 0x0a, 0x0c, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0b, 0x70, 0x61, + 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xde, 0x03, 0x0a, 0x1c, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x61, + 0x72, 0x67, 0x65, 0x74, 0x5f, 0x69, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, + 0x61, 0x72, 0x67, 0x65, 0x74, 0x49, 0x70, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x61, 0x74, + 0x74, 0x65, 0x6d, 0x70, 0x74, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x0c, 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, + 0x1b, 0x0a, 0x09, 0x65, 0x78, 0x69, 0x74, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x08, 0x65, 0x78, 0x69, 0x74, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x16, 0x0a, 0x06, + 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, + 0x74, 0x70, 0x75, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x72, + 0x65, 0x66, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, + 0x52, 0x65, 0x66, 0x12, 0x29, 0x0a, 0x10, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x72, + 0x75, 0x6e, 0x63, 0x61, 0x74, 0x65, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6f, + 0x75, 0x74, 0x70, 0x75, 0x74, 0x54, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x65, 0x64, 0x12, 0x1d, + 0x0a, 0x0a, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x23, 0x0a, + 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x0b, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, + 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x3b, 0x0a, + 0x0b, 0x66, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0d, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, + 0x66, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x41, 0x74, 0x22, 0xb5, 0x06, 0x0a, 0x16, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, + 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, + 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, + 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x2c, 0x0a, 0x12, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0b, 0x74, 0x6d, 0x73, 0x5f, + 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, + 0x6d, 0x73, 0x54, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x72, 0x74, 0x69, + 0x66, 0x61, 0x63, 0x74, 0x5f, 0x6d, 0x64, 0x35, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x4d, 0x64, 0x35, 0x12, 0x27, 0x0a, 0x0f, 0x6d, + 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x5f, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x18, 0x08, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x53, 0x68, + 0x61, 0x32, 0x35, 0x36, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x6d, 0x64, 0x5f, 0x73, + 0x68, 0x61, 0x32, 0x35, 0x36, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x70, + 0x6f, 0x6d, 0x64, 0x53, 0x68, 0x61, 0x32, 0x35, 0x36, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x5f, 0x62, + 0x79, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x65, 0x64, 0x42, 0x79, 0x12, 0x27, 0x0a, 0x0f, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x5f, + 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x74, + 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x1d, 0x0a, + 0x0a, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x23, 0x0a, 0x0d, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x0e, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x12, 0x51, 0x0a, 0x07, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x18, 0x0f, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x52, 0x07, 0x74, 0x61, 0x72, + 0x67, 0x65, 0x74, 0x73, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, + 0x61, 0x74, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, + 0x39, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x11, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x3b, 0x0a, 0x0b, 0x66, 0x69, + 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x66, 0x69, 0x6e, + 0x69, 0x73, 0x68, 0x65, 0x64, 0x41, 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x13, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, + 0x41, 0x74, 0x22, 0xb6, 0x01, 0x0a, 0x1e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x74, 0x72, + 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0c, + 0x64, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3d, 0x0a, 0x04, + 0x74, 0x61, 0x73, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x74, 0x72, 0x70, + 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x04, 0x74, 0x61, 0x73, 0x6b, 0x22, 0xc8, 0x01, 0x0a, 0x23, + 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x44, 0x69, + 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x0d, 0x64, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x74, 0x72, 0x70, + 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0d, 0x64, + 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x48, 0x0a, 0x0d, + 0x70, 0x61, 0x67, 0x65, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x61, 0x67, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0c, 0x70, 0x61, 0x67, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x7f, 0x0a, 0x27, 0x50, 0x72, 0x65, 0x76, 0x69, 0x65, + 0x77, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, 0x63, 0x6b, 0x61, + 0x67, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x2e, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, + 0x01, 0x18, 0x40, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, + 0x64, 0x12, 0x24, 0x0a, 0x06, 0x6e, 0x65, 0x76, 0x72, 0x61, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x09, 0x42, 0x0c, 0xfa, 0x42, 0x09, 0x92, 0x01, 0x06, 0x08, 0x01, 0x10, 0x64, 0x18, 0x01, 0x52, + 0x06, 0x6e, 0x65, 0x76, 0x72, 0x61, 0x73, 0x22, 0x84, 0x01, 0x0a, 0x27, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x72, 0x6d, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, 0x63, + 0x6b, 0x61, 0x67, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x0a, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, + 0x18, 0x40, 0x52, 0x09, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x49, 0x64, 0x12, 0x2f, 0x0a, + 0x0e, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x5f, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0x98, 0x01, 0x40, 0x52, + 0x0d, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x22, 0xa1, + 0x01, 0x0a, 0x26, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, 0x63, + 0x6b, 0x61, 0x67, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x65, 0x76, + 0x69, 0x65, 0x77, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, 0x12, 0x1f, + 0x0a, 0x0b, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, + 0x17, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x61, 0x73, 0x65, + 0x75, 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x61, 0x73, 0x65, 0x75, + 0x72, 0x6c, 0x22, 0xaa, 0x01, 0x0a, 0x21, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, + 0x63, 0x79, 0x49, 0x6d, 0x70, 0x61, 0x63, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x61, 0x63, 0x6b, + 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x65, 0x76, 0x72, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0c, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x4e, 0x65, 0x76, 0x72, 0x61, 0x12, 0x1a, 0x0a, + 0x08, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x64, 0x65, 0x70, + 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x64, 0x65, 0x70, 0x74, 0x68, 0x12, + 0x16, 0x0a, 0x06, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x06, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, + 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, + 0xc6, 0x04, 0x0a, 0x20, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, + 0x63, 0x6b, 0x61, 0x67, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x65, + 0x76, 0x69, 0x65, 0x77, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, + 0x77, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x5f, 0x64, + 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x70, 0x72, 0x65, + 0x76, 0x69, 0x65, 0x77, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, 0x12, + 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, + 0x12, 0x68, 0x0a, 0x0e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x73, + 0x65, 0x74, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x41, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, + 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, + 0x65, 0x76, 0x69, 0x65, 0x77, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x0d, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x65, + 0x76, 0x72, 0x61, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x6e, 0x65, 0x76, 0x72, + 0x61, 0x73, 0x12, 0x69, 0x0a, 0x11, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, + 0x5f, 0x69, 0x6d, 0x70, 0x61, 0x63, 0x74, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3c, 0x2e, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x44, 0x65, 0x70, 0x65, 0x6e, + 0x64, 0x65, 0x6e, 0x63, 0x79, 0x49, 0x6d, 0x70, 0x61, 0x63, 0x74, 0x52, 0x10, 0x64, 0x65, 0x70, + 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x49, 0x6d, 0x70, 0x61, 0x63, 0x74, 0x12, 0x18, 0x0a, + 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, + 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x61, 0x77, 0x5f, 0x6f, + 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x61, 0x77, + 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x39, + 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61, 0x74, 0x18, 0x0b, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, + 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x42, 0x79, 0x22, 0x81, 0x01, 0x0a, 0x28, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x07, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3b, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, 0x63, + 0x6b, 0x61, 0x67, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x65, 0x76, + 0x69, 0x65, 0x77, 0x52, 0x07, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x22, 0xa2, 0x02, 0x0a, + 0x26, 0x50, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, + 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x0b, 0x72, 0x65, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, + 0x04, 0x72, 0x02, 0x18, 0x40, 0x52, 0x0a, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x49, + 0x64, 0x12, 0x31, 0x0a, 0x0d, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x42, 0x0c, 0xfa, 0x42, 0x09, 0x92, 0x01, 0x06, + 0x08, 0x01, 0x10, 0x64, 0x18, 0x01, 0x52, 0x0c, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x4e, + 0x61, 0x6d, 0x65, 0x73, 0x12, 0x36, 0x0a, 0x10, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x42, 0x0c, + 0xfa, 0x42, 0x09, 0x92, 0x01, 0x06, 0x08, 0x01, 0x10, 0x64, 0x18, 0x01, 0x52, 0x0e, 0x74, 0x61, + 0x72, 0x67, 0x65, 0x74, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x73, 0x12, 0x33, 0x0a, 0x0f, + 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x05, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x1a, 0x05, 0x18, 0x88, 0x0e, 0x28, + 0x00, 0x52, 0x0e, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, + 0x73, 0x22, 0xcd, 0x05, 0x0a, 0x25, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x50, 0x72, + 0x65, 0x76, 0x69, 0x65, 0x77, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x02, 0x69, 0x70, 0x12, 0x47, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2f, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, + 0x6b, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, + 0x1b, 0x0a, 0x09, 0x65, 0x78, 0x69, 0x74, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x08, 0x65, 0x78, 0x69, 0x74, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1d, 0x0a, 0x0a, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x61, 0x77, 0x5f, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x61, 0x77, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, + 0x29, 0x0a, 0x10, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x72, 0x75, 0x6e, 0x63, 0x61, + 0x74, 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6f, 0x75, 0x74, 0x70, 0x75, + 0x74, 0x54, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x65, 0x64, 0x12, 0x39, 0x0a, 0x0a, 0x73, 0x74, + 0x61, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, + 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x3b, 0x0a, 0x0b, 0x66, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, + 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x66, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, + 0x41, 0x74, 0x12, 0x2b, 0x0a, 0x11, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x70, + 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x73, + 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x12, + 0x2f, 0x0a, 0x13, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x70, 0x61, + 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x64, 0x65, + 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, + 0x12, 0x29, 0x0a, 0x10, 0x75, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x5f, 0x70, 0x61, 0x63, 0x6b, + 0x61, 0x67, 0x65, 0x73, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x75, 0x70, 0x67, 0x72, + 0x61, 0x64, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x12, 0x31, 0x0a, 0x14, 0x72, + 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x61, + 0x67, 0x65, 0x73, 0x18, 0x0e, 0x20, 0x03, 0x28, 0x09, 0x52, 0x13, 0x72, 0x65, 0x70, 0x6c, 0x61, + 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x12, 0x25, + 0x0a, 0x0e, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, + 0x18, 0x0f, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, + 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x2a, 0x0a, 0x11, 0x64, 0x69, 0x73, 0x6b, 0x5f, 0x63, 0x68, + 0x61, 0x6e, 0x67, 0x65, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x10, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0f, 0x64, 0x69, 0x73, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x42, 0x79, 0x74, 0x65, + 0x73, 0x22, 0xb9, 0x03, 0x0a, 0x1f, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x50, 0x72, + 0x65, 0x76, 0x69, 0x65, 0x77, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x65, 0x76, 0x69, + 0x65, 0x77, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x5f, + 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x70, 0x72, + 0x65, 0x76, 0x69, 0x65, 0x77, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, + 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x49, + 0x64, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x61, + 0x63, 0x6b, 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x0c, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, + 0x5a, 0x0a, 0x07, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x40, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x49, 0x6e, + 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x50, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x54, 0x61, 0x72, 0x67, + 0x65, 0x74, 0x52, 0x07, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x12, 0x39, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61, + 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x1d, + 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x42, 0x79, 0x22, 0x7f, 0x0a, + 0x27, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, 0x63, 0x6b, 0x61, + 0x67, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x50, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x07, 0x70, 0x72, 0x65, 0x76, + 0x69, 0x65, 0x77, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x50, 0x72, + 0x65, 0x76, 0x69, 0x65, 0x77, 0x52, 0x07, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x22, 0x83, + 0x01, 0x0a, 0x26, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, + 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x0a, 0x70, 0x72, 0x65, + 0x76, 0x69, 0x65, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, + 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x09, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, + 0x77, 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x0e, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x5f, 0x64, + 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, 0x05, + 0x72, 0x03, 0x98, 0x01, 0x40, 0x52, 0x0d, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x44, 0x69, + 0x67, 0x65, 0x73, 0x74, 0x22, 0x53, 0x0a, 0x27, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, + 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x28, 0x0a, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x09, + 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x49, 0x64, 0x22, 0xd6, 0x03, 0x0a, 0x18, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x49, + 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, + 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, + 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0a, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x70, + 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x49, 0x64, 0x12, 0x2c, 0x0a, 0x12, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0b, 0x74, 0x6d, 0x73, 0x5f, + 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, + 0x6d, 0x73, 0x54, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x12, 0x27, 0x0a, 0x0f, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x6f, + 0x6e, 0x64, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x74, 0x69, 0x6d, 0x65, 0x6f, + 0x75, 0x74, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x72, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x42, 0x79, 0x12, 0x39, 0x0a, 0x0a, + 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, + 0x41, 0x74, 0x22, 0xb0, 0x01, 0x0a, 0x20, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x07, 0x69, 0x6e, 0x73, 0x74, 0x61, + 0x6c, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, + 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x52, 0x07, 0x69, + 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x12, 0x3d, 0x0a, 0x04, 0x74, 0x61, 0x73, 0x6b, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x52, + 0x04, 0x74, 0x61, 0x73, 0x6b, 0x22, 0x4a, 0x0a, 0x20, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x6e, 0x76, 0x65, 0x6e, 0x74, 0x6f, + 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0a, 0x70, 0x72, 0x6f, + 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xfa, + 0x42, 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, + 0x64, 0x22, 0x59, 0x0a, 0x27, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x0d, + 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x0c, + 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, 0x22, 0x5f, 0x0a, 0x29, + 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, + 0x6c, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x0f, 0x64, 0x69, 0x73, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x0e, 0x64, + 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0x8c, 0x01, + 0x0a, 0x18, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x45, 0x78, 0x70, 0x6f, + 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x69, + 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, + 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, + 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, + 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x68, + 0x61, 0x32, 0x35, 0x36, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x68, 0x61, 0x32, + 0x35, 0x36, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x22, 0x92, 0x01, 0x0a, + 0x25, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x6e, 0x76, 0x65, 0x6e, + 0x74, 0x6f, 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x4d, + 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x29, 0x0a, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x5f, 0x72, 0x65, 0x66, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x72, + 0x05, 0x10, 0x01, 0x18, 0x80, 0x01, 0x52, 0x09, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, + 0x66, 0x12, 0x3e, 0x0a, 0x16, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x73, 0x74, 0x6f, 0x72, + 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x13, 0x74, 0x61, + 0x72, 0x67, 0x65, 0x74, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x49, + 0x64, 0x22, 0x88, 0x01, 0x0a, 0x24, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x49, 0x6e, 0x76, 0x65, 0x6e, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, + 0x4b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x30, 0x0a, 0x0e, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x72, 0x65, 0x66, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0x80, 0x01, 0x52, 0x0c, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x66, 0x12, 0x2e, 0x0a, 0x0e, + 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x72, 0x65, 0x66, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0x18, 0x80, 0x01, 0x52, 0x0c, + 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x66, 0x22, 0x8f, 0x04, 0x0a, + 0x27, 0x50, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x49, 0x6e, 0x76, 0x65, 0x6e, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x6d, 0x70, 0x6f, 0x72, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, + 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xfa, 0x42, + 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, + 0x12, 0x26, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x42, 0x0c, 0xfa, 0x42, 0x09, 0x72, 0x07, 0x10, 0x01, 0x18, 0x80, 0x80, 0x80, 0x05, 0x52, + 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x6b, 0x0a, 0x11, 0x63, 0x6f, 0x6e, 0x66, + 0x6c, 0x69, 0x63, 0x74, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x3e, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x6e, 0x76, 0x65, 0x6e, 0x74, + 0x6f, 0x72, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x53, 0x74, 0x72, 0x61, 0x74, + 0x65, 0x67, 0x79, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x53, 0x74, 0x72, + 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x3e, 0x0a, 0x17, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, + 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x18, 0x40, 0x52, + 0x14, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, + 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x74, 0x0a, 0x15, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, + 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x05, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x40, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x6e, 0x76, 0x65, 0x6e, + 0x74, 0x6f, 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x4d, + 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x13, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, + 0x6f, 0x64, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x71, 0x0a, 0x14, 0x73, + 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, + 0x6e, 0x67, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3f, 0x2e, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x49, 0x6e, 0x76, 0x65, 0x6e, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, + 0x4b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x12, 0x73, 0x69, 0x67, 0x6e, + 0x69, 0x6e, 0x67, 0x4b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x22, 0xe3, + 0x01, 0x0a, 0x1d, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x6e, 0x76, + 0x65, 0x6e, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x74, 0x65, 0x6d, + 0x12, 0x30, 0x0a, 0x14, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4e, + 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x6d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x3d, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, + 0x73, 0x70, 0x65, 0x63, 0x22, 0x81, 0x03, 0x0a, 0x28, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x49, 0x6e, 0x76, 0x65, 0x6e, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x6d, 0x70, 0x6f, + 0x72, 0x74, 0x50, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x49, 0x64, + 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, + 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x72, 0x65, 0x76, 0x69, + 0x65, 0x77, 0x5f, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0d, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x12, 0x29, + 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x39, 0x0a, 0x0a, 0x65, 0x78, 0x70, + 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, + 0x65, 0x73, 0x41, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x4e, 0x0a, 0x05, + 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x74, 0x72, + 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x49, 0x6e, 0x76, 0x65, 0x6e, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x6d, 0x70, 0x6f, 0x72, + 0x74, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x1a, 0x0a, 0x08, + 0x77, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, + 0x77, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x22, 0x84, 0x01, 0x0a, 0x27, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x72, 0x6d, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x6e, + 0x76, 0x65, 0x6e, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x0a, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, + 0x01, 0x18, 0x40, 0x52, 0x09, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x49, 0x64, 0x12, 0x2f, + 0x0a, 0x0e, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x5f, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0x98, 0x01, 0x40, + 0x52, 0x0d, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x22, + 0xe5, 0x03, 0x0a, 0x1c, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x6e, + 0x76, 0x65, 0x6e, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x4a, 0x6f, 0x62, + 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, + 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, + 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x49, 0x64, 0x12, 0x25, + 0x0a, 0x0e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x56, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2b, 0x0a, 0x11, 0x63, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, + 0x74, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, + 0x67, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x21, + 0x0a, 0x0c, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x18, 0x09, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x42, + 0x79, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, + 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x3b, 0x0a, 0x0b, + 0x66, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x66, + 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x41, 0x74, 0x12, 0x2c, 0x0a, 0x12, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, + 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x54, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x22, 0xb9, 0x01, 0x0a, 0x21, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x6e, 0x76, 0x65, 0x6e, 0x74, 0x6f, 0x72, 0x79, 0x49, + 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, + 0x03, 0x6a, 0x6f, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x74, 0x72, 0x70, + 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x49, 0x6e, 0x76, 0x65, 0x6e, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, + 0x4a, 0x6f, 0x62, 0x52, 0x03, 0x6a, 0x6f, 0x62, 0x12, 0x49, 0x0a, 0x0c, 0x72, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, + 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, + 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x69, 0x65, 0x73, 0x22, 0x8a, 0x01, 0x0a, 0x18, 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x65, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x2e, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, + 0x18, 0x40, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, + 0x12, 0x3e, 0x0a, 0x16, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, + 0x67, 0x65, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x13, 0x74, 0x61, 0x72, + 0x67, 0x65, 0x74, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x64, + 0x22, 0x46, 0x0a, 0x20, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x07, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, + 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x22, 0x46, 0x0a, 0x20, 0x44, 0x6f, 0x77, 0x6e, + 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, + 0x73, 0x6b, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x07, + 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, + 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, + 0x22, 0xa2, 0x03, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, + 0x61, 0x73, 0x6b, 0x4c, 0x6f, 0x67, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x12, + 0x26, 0x0a, 0x0f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, + 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, + 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x68, 0x61, 0x32, 0x35, + 0x36, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x12, + 0x29, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x5f, 0x62, 0x79, + 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x63, 0x6f, 0x6d, 0x70, 0x72, + 0x65, 0x73, 0x73, 0x65, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x12, 0x75, 0x6e, + 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x75, 0x6e, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, + 0x73, 0x73, 0x65, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x6c, 0x69, 0x6e, + 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x6c, + 0x69, 0x6e, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x6d, 0x6d, + 0x61, 0x72, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, + 0x72, 0x79, 0x12, 0x2b, 0x0a, 0x11, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x66, 0x69, 0x6e, 0x67, + 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x46, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x12, + 0x39, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61, 0x74, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0x64, 0x0a, 0x19, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x47, 0x0a, 0x08, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x6c, 0x6f, 0x67, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x4c, + 0x6f, 0x67, 0x52, 0x07, 0x74, 0x61, 0x73, 0x6b, 0x4c, 0x6f, 0x67, 0x22, 0xd4, 0x04, 0x0a, 0x1d, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, + 0x61, 0x6d, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x53, 0x70, 0x65, 0x63, 0x12, 0x26, 0x0a, + 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x03, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, + 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0x80, 0x01, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2a, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, + 0x03, 0x18, 0x80, 0x08, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x31, 0x0a, 0x0e, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x5f, 0x70, 0x72, 0x6f, 0x64, + 0x75, 0x63, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x72, 0x05, + 0x10, 0x01, 0x18, 0x80, 0x01, 0x52, 0x0d, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x50, 0x72, 0x6f, + 0x64, 0x75, 0x63, 0x74, 0x12, 0x23, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, + 0x52, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x2d, 0x0a, 0x0c, 0x75, 0x72, 0x6c, + 0x5f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x0a, 0xfa, 0x42, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0x80, 0x10, 0x52, 0x0b, 0x75, 0x72, 0x6c, + 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x12, 0x2d, 0x0a, 0x12, 0x73, 0x75, 0x70, 0x70, + 0x6f, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x07, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x11, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x56, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x6a, 0x0a, 0x17, 0x73, 0x75, 0x70, 0x70, 0x6f, + 0x72, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, + 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x31, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x41, + 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x52, 0x16, 0x73, 0x75, 0x70, + 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x41, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, + 0x72, 0x65, 0x73, 0x12, 0x38, 0x0a, 0x18, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x69, + 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x18, + 0x09, 0x20, 0x03, 0x28, 0x09, 0x52, 0x16, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x49, 0x6e, + 0x63, 0x6c, 0x75, 0x64, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x12, 0x38, 0x0a, + 0x18, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, + 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x16, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x45, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x50, + 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x64, 0x22, 0xa2, 0x05, 0x0a, 0x19, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, + 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, + 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, + 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x5f, + 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, + 0x79, 0x73, 0x74, 0x65, 0x6d, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x12, 0x18, 0x0a, 0x07, + 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, + 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x21, 0x0a, 0x0c, 0x75, 0x72, 0x6c, 0x5f, 0x74, 0x65, + 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x75, 0x72, + 0x6c, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x12, 0x2d, 0x0a, 0x12, 0x73, 0x75, 0x70, + 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, + 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x11, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, + 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x6a, 0x0a, 0x17, 0x73, 0x75, 0x70, 0x70, + 0x6f, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, + 0x72, 0x65, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x31, 0x2e, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x41, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x52, 0x16, 0x73, 0x75, + 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x41, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, + 0x75, 0x72, 0x65, 0x73, 0x12, 0x38, 0x0a, 0x18, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, + 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, + 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x16, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x49, + 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x12, 0x38, + 0x0a, 0x18, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, + 0x65, 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x16, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x45, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, + 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79, + 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x42, + 0x79, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, + 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x39, 0x0a, 0x0a, + 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x75, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0x76, 0x0a, 0x26, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x73, 0x74, 0x72, + 0x65, 0x61, 0x6d, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x4c, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x38, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, + 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x72, + 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x22, + 0xb1, 0x01, 0x0a, 0x26, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x72, 0x6f, 0x66, + 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x39, 0x0a, 0x13, 0x75, 0x70, + 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, + 0x18, 0x40, 0x52, 0x11, 0x75, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x72, 0x6f, 0x66, + 0x69, 0x6c, 0x65, 0x49, 0x64, 0x12, 0x4c, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, + 0x61, 0x6d, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, + 0x70, 0x65, 0x63, 0x22, 0x65, 0x0a, 0x28, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, + 0x6d, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x39, 0x0a, 0x13, 0x75, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x70, 0x72, 0x6f, 0x66, + 0x69, 0x6c, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, + 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x11, 0x75, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, + 0x6d, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x49, 0x64, 0x22, 0x96, 0x02, 0x0a, 0x25, 0x4c, + 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x73, + 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x22, 0x02, 0x20, + 0x00, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x0e, + 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x5f, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0x18, 0x80, 0x01, 0x52, 0x0d, + 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x12, 0x22, 0x0a, + 0x07, 0x6b, 0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, + 0xfa, 0x42, 0x05, 0x72, 0x03, 0x18, 0x80, 0x01, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x77, 0x6f, 0x72, + 0x64, 0x12, 0x1d, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x08, 0x48, 0x00, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x88, 0x01, 0x01, + 0x12, 0x45, 0x0a, 0x0c, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, + 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0b, 0x70, 0x61, 0x67, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x65, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x22, 0x63, 0x0a, 0x26, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, + 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x39, 0x0a, + 0x13, 0x75, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, + 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, + 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x11, 0x75, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, + 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x49, 0x64, 0x22, 0x73, 0x0a, 0x21, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x72, + 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, + 0x07, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, + 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, + 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x72, 0x6f, + 0x66, 0x69, 0x6c, 0x65, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x22, 0xc4, 0x01, + 0x0a, 0x26, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x50, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x66, + 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x74, 0x72, 0x70, + 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, + 0x52, 0x08, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x48, 0x0a, 0x0d, 0x70, 0x61, + 0x67, 0x65, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x23, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0c, 0x70, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x2a, 0x88, 0x01, 0x0a, 0x0e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1f, 0x0a, 0x1b, 0x52, 0x45, 0x50, 0x4f, 0x53, + 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, + 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x52, 0x45, 0x50, 0x4f, + 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x42, 0x41, 0x53, 0x45, + 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x01, 0x12, 0x1a, 0x0a, 0x16, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, + 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, + 0x10, 0x02, 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, + 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x50, 0x52, 0x49, 0x56, 0x41, 0x54, 0x45, 0x10, 0x03, 0x2a, + 0xae, 0x01, 0x0a, 0x16, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x41, 0x72, + 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x12, 0x27, 0x0a, 0x23, 0x52, 0x45, + 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x41, 0x52, 0x43, 0x48, 0x49, 0x54, 0x45, + 0x43, 0x54, 0x55, 0x52, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, + 0x44, 0x10, 0x00, 0x12, 0x22, 0x0a, 0x1e, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, + 0x59, 0x5f, 0x41, 0x52, 0x43, 0x48, 0x49, 0x54, 0x45, 0x43, 0x54, 0x55, 0x52, 0x45, 0x5f, 0x58, + 0x38, 0x36, 0x5f, 0x36, 0x34, 0x10, 0x01, 0x12, 0x23, 0x0a, 0x1f, 0x52, 0x45, 0x50, 0x4f, 0x53, + 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x41, 0x52, 0x43, 0x48, 0x49, 0x54, 0x45, 0x43, 0x54, 0x55, + 0x52, 0x45, 0x5f, 0x41, 0x41, 0x52, 0x43, 0x48, 0x36, 0x34, 0x10, 0x02, 0x12, 0x22, 0x0a, 0x1e, + 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x41, 0x52, 0x43, 0x48, 0x49, + 0x54, 0x45, 0x43, 0x54, 0x55, 0x52, 0x45, 0x5f, 0x4e, 0x4f, 0x41, 0x52, 0x43, 0x48, 0x10, 0x03, + 0x2a, 0xce, 0x01, 0x0a, 0x13, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x47, + 0x50, 0x47, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x25, 0x0a, 0x21, 0x52, 0x45, 0x50, 0x4f, + 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x47, 0x50, 0x47, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43, + 0x59, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, + 0x22, 0x0a, 0x1e, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x47, 0x50, + 0x47, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, + 0x44, 0x10, 0x01, 0x12, 0x28, 0x0a, 0x24, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, + 0x59, 0x5f, 0x47, 0x50, 0x47, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f, 0x56, 0x45, 0x52, + 0x49, 0x46, 0x59, 0x5f, 0x50, 0x41, 0x43, 0x4b, 0x41, 0x47, 0x45, 0x10, 0x02, 0x12, 0x1f, 0x0a, + 0x1b, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x47, 0x50, 0x47, 0x5f, + 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f, 0x41, 0x55, 0x44, 0x49, 0x54, 0x10, 0x03, 0x12, 0x21, + 0x0a, 0x1d, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x47, 0x50, 0x47, + 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f, 0x45, 0x4e, 0x46, 0x4f, 0x52, 0x43, 0x45, 0x10, + 0x04, 0x2a, 0x7c, 0x0a, 0x12, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, + 0x79, 0x6e, 0x63, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x24, 0x0a, 0x20, 0x52, 0x45, 0x50, 0x4f, 0x53, + 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x53, 0x59, 0x4e, 0x43, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, + 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1f, 0x0a, + 0x1b, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x53, 0x59, 0x4e, 0x43, + 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x4d, 0x49, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x01, 0x12, 0x1f, + 0x0a, 0x1b, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x53, 0x59, 0x4e, + 0x43, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x41, 0x50, 0x50, 0x45, 0x4e, 0x44, 0x10, 0x02, 0x2a, + 0xf2, 0x01, 0x0a, 0x23, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x6e, + 0x76, 0x65, 0x6e, 0x74, 0x6f, 0x72, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x53, + 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x36, 0x0a, 0x32, 0x52, 0x45, 0x50, 0x4f, 0x53, + 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x49, 0x4e, 0x56, 0x45, 0x4e, 0x54, 0x4f, 0x52, 0x59, 0x5f, + 0x43, 0x4f, 0x4e, 0x46, 0x4c, 0x49, 0x43, 0x54, 0x5f, 0x53, 0x54, 0x52, 0x41, 0x54, 0x45, 0x47, + 0x59, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, + 0x2f, 0x0a, 0x2b, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x49, 0x4e, + 0x56, 0x45, 0x4e, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x4c, 0x49, 0x43, 0x54, + 0x5f, 0x53, 0x54, 0x52, 0x41, 0x54, 0x45, 0x47, 0x59, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x10, 0x01, + 0x12, 0x2f, 0x0a, 0x2b, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x49, + 0x4e, 0x56, 0x45, 0x4e, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x4c, 0x49, 0x43, + 0x54, 0x5f, 0x53, 0x54, 0x52, 0x41, 0x54, 0x45, 0x47, 0x59, 0x5f, 0x53, 0x4b, 0x49, 0x50, 0x10, + 0x02, 0x12, 0x31, 0x0a, 0x2d, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, + 0x49, 0x4e, 0x56, 0x45, 0x4e, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x4c, 0x49, + 0x43, 0x54, 0x5f, 0x53, 0x54, 0x52, 0x41, 0x54, 0x45, 0x47, 0x59, 0x5f, 0x52, 0x45, 0x4e, 0x41, + 0x4d, 0x45, 0x10, 0x03, 0x2a, 0xaa, 0x01, 0x0a, 0x1d, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x43, + 0x61, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x30, 0x0a, 0x2c, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, + 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x53, 0x59, 0x4e, 0x43, 0x5f, 0x53, 0x43, 0x48, 0x45, 0x44, 0x55, + 0x4c, 0x45, 0x5f, 0x43, 0x41, 0x44, 0x45, 0x4e, 0x43, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, + 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x2a, 0x0a, 0x26, 0x52, 0x45, 0x50, 0x4f, + 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x53, 0x59, 0x4e, 0x43, 0x5f, 0x53, 0x43, 0x48, 0x45, + 0x44, 0x55, 0x4c, 0x45, 0x5f, 0x43, 0x41, 0x44, 0x45, 0x4e, 0x43, 0x45, 0x5f, 0x44, 0x41, 0x49, + 0x4c, 0x59, 0x10, 0x01, 0x12, 0x2b, 0x0a, 0x27, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, + 0x52, 0x59, 0x5f, 0x53, 0x59, 0x4e, 0x43, 0x5f, 0x53, 0x43, 0x48, 0x45, 0x44, 0x55, 0x4c, 0x45, + 0x5f, 0x43, 0x41, 0x44, 0x45, 0x4e, 0x43, 0x45, 0x5f, 0x57, 0x45, 0x45, 0x4b, 0x4c, 0x59, 0x10, + 0x02, 0x2a, 0xd9, 0x01, 0x0a, 0x1c, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x53, 0x79, 0x6e, 0x63, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x12, 0x2f, 0x0a, 0x2b, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, + 0x5f, 0x53, 0x59, 0x4e, 0x43, 0x5f, 0x53, 0x43, 0x48, 0x45, 0x44, 0x55, 0x4c, 0x45, 0x5f, 0x53, + 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, + 0x44, 0x10, 0x00, 0x12, 0x2b, 0x0a, 0x27, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, + 0x59, 0x5f, 0x53, 0x59, 0x4e, 0x43, 0x5f, 0x53, 0x43, 0x48, 0x45, 0x44, 0x55, 0x4c, 0x45, 0x5f, + 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x4e, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x10, 0x01, + 0x12, 0x2a, 0x0a, 0x26, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x53, + 0x59, 0x4e, 0x43, 0x5f, 0x53, 0x43, 0x48, 0x45, 0x44, 0x55, 0x4c, 0x45, 0x5f, 0x53, 0x54, 0x41, + 0x54, 0x55, 0x53, 0x5f, 0x50, 0x41, 0x55, 0x53, 0x45, 0x44, 0x10, 0x02, 0x12, 0x2f, 0x0a, 0x2b, + 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x53, 0x59, 0x4e, 0x43, 0x5f, + 0x53, 0x43, 0x48, 0x45, 0x44, 0x55, 0x4c, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, + 0x41, 0x55, 0x54, 0x4f, 0x5f, 0x50, 0x41, 0x55, 0x53, 0x45, 0x44, 0x10, 0x03, 0x2a, 0x8d, 0x04, + 0x0a, 0x12, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x24, 0x0a, 0x20, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, + 0x52, 0x59, 0x5f, 0x54, 0x41, 0x53, 0x4b, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, + 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1d, 0x0a, 0x19, 0x52, 0x45, + 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x54, 0x41, 0x53, 0x4b, 0x5f, 0x54, 0x59, + 0x50, 0x45, 0x5f, 0x53, 0x59, 0x4e, 0x43, 0x10, 0x01, 0x12, 0x1f, 0x0a, 0x1b, 0x52, 0x45, 0x50, + 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x54, 0x41, 0x53, 0x4b, 0x5f, 0x54, 0x59, 0x50, + 0x45, 0x5f, 0x55, 0x50, 0x4c, 0x4f, 0x41, 0x44, 0x10, 0x02, 0x12, 0x27, 0x0a, 0x23, 0x52, 0x45, + 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x54, 0x41, 0x53, 0x4b, 0x5f, 0x54, 0x59, + 0x50, 0x45, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x50, 0x41, 0x43, 0x4b, 0x41, 0x47, + 0x45, 0x10, 0x03, 0x12, 0x29, 0x0a, 0x25, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, + 0x59, 0x5f, 0x54, 0x41, 0x53, 0x4b, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x52, 0x45, 0x42, 0x55, + 0x49, 0x4c, 0x44, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x44, 0x41, 0x54, 0x41, 0x10, 0x04, 0x12, 0x2a, + 0x0a, 0x26, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x54, 0x41, 0x53, + 0x4b, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x52, 0x45, + 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x10, 0x05, 0x12, 0x20, 0x0a, 0x1c, 0x52, 0x45, + 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x54, 0x41, 0x53, 0x4b, 0x5f, 0x54, 0x59, + 0x50, 0x45, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x53, 0x48, 0x10, 0x06, 0x12, 0x21, 0x0a, 0x1d, + 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x54, 0x41, 0x53, 0x4b, 0x5f, + 0x54, 0x59, 0x50, 0x45, 0x5f, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x41, 0x54, 0x45, 0x10, 0x07, 0x12, + 0x23, 0x0a, 0x1f, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x54, 0x41, + 0x53, 0x4b, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x49, 0x53, 0x54, 0x52, 0x49, 0x42, 0x55, + 0x54, 0x45, 0x10, 0x08, 0x12, 0x2b, 0x0a, 0x27, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, + 0x52, 0x59, 0x5f, 0x54, 0x41, 0x53, 0x4b, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4d, 0x49, 0x47, + 0x52, 0x41, 0x54, 0x45, 0x5f, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x10, + 0x09, 0x12, 0x25, 0x0a, 0x21, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, + 0x54, 0x41, 0x53, 0x4b, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4e, 0x4f, 0x44, 0x45, 0x5f, 0x49, + 0x4e, 0x53, 0x54, 0x41, 0x4c, 0x4c, 0x10, 0x0a, 0x12, 0x28, 0x0a, 0x24, 0x52, 0x45, 0x50, 0x4f, + 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x54, 0x41, 0x53, 0x4b, 0x5f, 0x54, 0x59, 0x50, 0x45, + 0x5f, 0x50, 0x41, 0x43, 0x4b, 0x41, 0x47, 0x45, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4c, 0x4c, + 0x10, 0x0b, 0x12, 0x29, 0x0a, 0x25, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, + 0x5f, 0x54, 0x41, 0x53, 0x4b, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x49, 0x4e, 0x56, 0x45, 0x4e, + 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x49, 0x4d, 0x50, 0x4f, 0x52, 0x54, 0x10, 0x0c, 0x2a, 0xcb, 0x02, + 0x0a, 0x14, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x26, 0x0a, 0x22, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, + 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x54, 0x41, 0x53, 0x4b, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, + 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x22, + 0x0a, 0x1e, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x54, 0x41, 0x53, + 0x4b, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, + 0x10, 0x01, 0x12, 0x22, 0x0a, 0x1e, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, + 0x5f, 0x54, 0x41, 0x53, 0x4b, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x52, 0x55, 0x4e, + 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x2b, 0x0a, 0x27, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, + 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x54, 0x41, 0x53, 0x4b, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, + 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x45, 0x53, 0x54, 0x45, + 0x44, 0x10, 0x03, 0x12, 0x23, 0x0a, 0x1f, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, + 0x59, 0x5f, 0x54, 0x41, 0x53, 0x4b, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x43, 0x41, + 0x4e, 0x43, 0x45, 0x4c, 0x45, 0x44, 0x10, 0x04, 0x12, 0x22, 0x0a, 0x1e, 0x52, 0x45, 0x50, 0x4f, + 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x54, 0x41, 0x53, 0x4b, 0x5f, 0x53, 0x54, 0x41, 0x54, + 0x55, 0x53, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x05, 0x12, 0x21, 0x0a, 0x1d, + 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x54, 0x41, 0x53, 0x4b, 0x5f, + 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x06, 0x12, + 0x2a, 0x0a, 0x26, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x54, 0x41, + 0x53, 0x4b, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x50, 0x41, 0x52, 0x54, 0x49, 0x41, + 0x4c, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x07, 0x2a, 0x8b, 0x03, 0x0a, 0x1b, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, + 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x2e, 0x0a, 0x2a, 0x52, + 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x53, 0x54, 0x4f, 0x52, 0x41, 0x47, + 0x45, 0x5f, 0x4e, 0x4f, 0x44, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, + 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x2a, 0x0a, 0x26, 0x52, + 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x53, 0x54, 0x4f, 0x52, 0x41, 0x47, + 0x45, 0x5f, 0x4e, 0x4f, 0x44, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, + 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x01, 0x12, 0x29, 0x0a, 0x25, 0x52, 0x45, 0x50, 0x4f, 0x53, + 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x53, 0x54, 0x4f, 0x52, 0x41, 0x47, 0x45, 0x5f, 0x4e, 0x4f, + 0x44, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x4f, 0x4e, 0x4c, 0x49, 0x4e, 0x45, + 0x10, 0x02, 0x12, 0x2a, 0x0a, 0x26, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, + 0x5f, 0x53, 0x54, 0x4f, 0x52, 0x41, 0x47, 0x45, 0x5f, 0x4e, 0x4f, 0x44, 0x45, 0x5f, 0x53, 0x54, + 0x41, 0x54, 0x55, 0x53, 0x5f, 0x4f, 0x46, 0x46, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x03, 0x12, 0x2b, + 0x0a, 0x27, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x53, 0x54, 0x4f, + 0x52, 0x41, 0x47, 0x45, 0x5f, 0x4e, 0x4f, 0x44, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, + 0x5f, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x10, 0x04, 0x12, 0x2a, 0x0a, 0x26, 0x52, + 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x53, 0x54, 0x4f, 0x52, 0x41, 0x47, + 0x45, 0x5f, 0x4e, 0x4f, 0x44, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x50, 0x45, + 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x05, 0x12, 0x2d, 0x0a, 0x29, 0x52, 0x45, 0x50, 0x4f, 0x53, + 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x53, 0x54, 0x4f, 0x52, 0x41, 0x47, 0x45, 0x5f, 0x4e, 0x4f, + 0x44, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4c, + 0x4c, 0x49, 0x4e, 0x47, 0x10, 0x06, 0x12, 0x31, 0x0a, 0x2d, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, + 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x53, 0x54, 0x4f, 0x52, 0x41, 0x47, 0x45, 0x5f, 0x4e, 0x4f, 0x44, + 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4c, 0x4c, + 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x07, 0x2a, 0x8a, 0x04, 0x0a, 0x1e, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x45, 0x6e, 0x72, 0x6f, + 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x31, 0x0a, 0x2d, + 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x4e, 0x4f, 0x44, 0x45, 0x5f, + 0x45, 0x4e, 0x52, 0x4f, 0x4c, 0x4c, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, + 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, + 0x2d, 0x0a, 0x29, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x4e, 0x4f, + 0x44, 0x45, 0x5f, 0x45, 0x4e, 0x52, 0x4f, 0x4c, 0x4c, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x54, + 0x41, 0x54, 0x55, 0x53, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x30, + 0x0a, 0x2c, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x4e, 0x4f, 0x44, + 0x45, 0x5f, 0x45, 0x4e, 0x52, 0x4f, 0x4c, 0x4c, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x54, 0x41, + 0x54, 0x55, 0x53, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4c, 0x4c, 0x49, 0x4e, 0x47, 0x10, 0x02, + 0x12, 0x32, 0x0a, 0x2e, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x4e, + 0x4f, 0x44, 0x45, 0x5f, 0x45, 0x4e, 0x52, 0x4f, 0x4c, 0x4c, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, + 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x42, 0x4f, 0x4f, 0x54, 0x53, 0x54, 0x52, 0x41, 0x50, 0x50, + 0x45, 0x44, 0x10, 0x03, 0x12, 0x2c, 0x0a, 0x28, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, + 0x52, 0x59, 0x5f, 0x4e, 0x4f, 0x44, 0x45, 0x5f, 0x45, 0x4e, 0x52, 0x4f, 0x4c, 0x4c, 0x4d, 0x45, + 0x4e, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x4f, 0x4e, 0x4c, 0x49, 0x4e, 0x45, + 0x10, 0x04, 0x12, 0x2c, 0x0a, 0x28, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, + 0x5f, 0x4e, 0x4f, 0x44, 0x45, 0x5f, 0x45, 0x4e, 0x52, 0x4f, 0x4c, 0x4c, 0x4d, 0x45, 0x4e, 0x54, + 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x05, + 0x12, 0x2d, 0x0a, 0x29, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x4e, + 0x4f, 0x44, 0x45, 0x5f, 0x45, 0x4e, 0x52, 0x4f, 0x4c, 0x4c, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, + 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x58, 0x50, 0x49, 0x52, 0x45, 0x44, 0x10, 0x06, 0x12, + 0x30, 0x0a, 0x2c, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x4e, 0x4f, + 0x44, 0x45, 0x5f, 0x45, 0x4e, 0x52, 0x4f, 0x4c, 0x4c, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x54, + 0x41, 0x54, 0x55, 0x53, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x5a, 0x45, 0x44, 0x10, + 0x07, 0x12, 0x31, 0x0a, 0x2d, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x5f, + 0x4e, 0x4f, 0x44, 0x45, 0x5f, 0x45, 0x4e, 0x52, 0x4f, 0x4c, 0x4c, 0x4d, 0x45, 0x4e, 0x54, 0x5f, + 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x49, + 0x4e, 0x47, 0x10, 0x08, 0x12, 0x30, 0x0a, 0x2c, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, + 0x52, 0x59, 0x5f, 0x4e, 0x4f, 0x44, 0x45, 0x5f, 0x45, 0x4e, 0x52, 0x4f, 0x4c, 0x4c, 0x4d, 0x45, + 0x4e, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, + 0x45, 0x52, 0x45, 0x44, 0x10, 0x09, 0x32, 0xcc, 0x55, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x75, 0x0a, 0x10, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x12, 0x32, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x75, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x32, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, + 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x74, 0x72, + 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x79, 0x0a, 0x12, 0x44, 0x65, + 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x12, 0x34, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x44, 0x65, 0x73, + 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7b, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x65, 0x73, 0x12, 0x32, 0x2e, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x5e, 0x0a, 0x10, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x32, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x12, 0x8d, 0x01, 0x0a, 0x16, 0x42, 0x61, 0x74, 0x63, 0x68, 0x41, 0x70, 0x70, 0x6c, + 0x79, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x65, 0x73, 0x12, 0x38, 0x2e, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x41, + 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x65, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x39, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, + 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0xa2, 0x01, 0x0a, 0x1f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, + 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x12, 0x41, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x72, 0x6f, 0x66, 0x69, + 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3c, 0x2e, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xa2, 0x01, 0x0a, 0x1f, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x73, 0x74, + 0x72, 0x65, 0x61, 0x6d, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x12, 0x41, 0x2e, 0x74, 0x72, + 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, + 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3c, + 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, + 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x72, 0x6f, + 0x66, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xa6, 0x01, 0x0a, + 0x21, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x72, 0x6f, 0x66, 0x69, + 0x6c, 0x65, 0x12, 0x43, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x44, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3c, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, + 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, + 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xa5, 0x01, 0x0a, 0x1e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, + 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x40, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x72, 0x6f, 0x66, 0x69, + 0x6c, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x41, 0x2e, 0x74, 0x72, 0x70, + 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x72, 0x6f, + 0x66, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7c, 0x0a, + 0x1f, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, + 0x12, 0x41, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x73, + 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x96, 0x01, 0x0a, 0x1b, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x3d, 0x2e, 0x74, 0x72, + 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, + 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x38, 0x2e, 0x74, 0x72, 0x70, + 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x96, 0x01, 0x0a, 0x1b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, + 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x3d, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x38, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, + 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x9a, 0x01, + 0x0a, 0x1d, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x12, + 0x3f, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, + 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x44, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x74, + 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x38, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, + 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x99, 0x01, 0x0a, 0x1a, 0x4c, + 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x73, 0x12, 0x3c, 0x2e, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3d, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, + 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x74, 0x0a, 0x1b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, + 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x3d, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x99, 0x01, 0x0a, + 0x1a, 0x50, 0x72, 0x6f, 0x62, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x3c, 0x2e, 0x74, 0x72, + 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x62, 0x65, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, + 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3d, 0x2e, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x50, 0x72, 0x6f, 0x62, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xa5, 0x01, 0x0a, 0x1e, 0x4c, 0x69, 0x73, + 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x72, 0x65, 0x66, 0x6c, + 0x69, 0x67, 0x68, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x40, 0x2e, 0x74, 0x72, + 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x72, 0x65, 0x66, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x52, + 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x41, 0x2e, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x72, 0x65, 0x66, 0x6c, 0x69, 0x67, 0x68, + 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x9f, 0x01, 0x0a, 0x1e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, + 0x65, 0x6e, 0x74, 0x12, 0x40, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x4e, 0x6f, 0x64, 0x65, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3b, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, + 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x9d, 0x01, 0x0a, 0x1e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x72, 0x74, + 0x69, 0x66, 0x61, 0x63, 0x74, 0x12, 0x40, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x39, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, + 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, + 0x64, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x9c, 0x01, 0x0a, 0x1b, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, + 0x74, 0x73, 0x12, 0x3d, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, + 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x3e, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x4c, 0x69, + 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, + 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x7a, 0x0a, 0x1e, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, + 0x61, 0x63, 0x74, 0x12, 0x40, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, + 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0xae, 0x01, + 0x0a, 0x24, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, + 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x46, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, + 0x6c, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3e, + 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, + 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, + 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xab, + 0x01, 0x0a, 0x20, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x53, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x73, 0x12, 0x42, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, + 0x64, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x43, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, + 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x53, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xc0, 0x01, 0x0a, + 0x29, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x6c, 0x69, 0x73, 0x74, 0x12, 0x4b, 0x2e, 0x74, 0x72, 0x70, + 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x6c, 0x69, 0x73, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x46, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, + 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, + 0x64, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x6c, + 0x6c, 0x6f, 0x77, 0x6c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0xc3, 0x01, 0x0a, 0x28, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x6c, 0x69, 0x73, 0x74, 0x73, 0x12, 0x4a, 0x2e, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x6c, 0x69, 0x73, 0x74, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x4b, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x6c, 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x75, 0x0a, 0x0e, 0x53, 0x79, 0x6e, 0x63, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x30, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, + 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x2e, 0x53, 0x79, 0x6e, 0x63, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x99, 0x01, 0x0a, + 0x1c, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x3e, 0x2e, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x63, + 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x39, 0x2e, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x9d, 0x01, 0x0a, 0x1e, 0x44, 0x65, 0x73, + 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, + 0x79, 0x6e, 0x63, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x40, 0x2e, 0x74, 0x72, + 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x63, + 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x39, 0x2e, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x9e, 0x01, 0x0a, 0x1b, 0x50, 0x61, 0x75, + 0x73, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x79, 0x6e, 0x63, + 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x44, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, + 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x39, + 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, + 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x9f, 0x01, 0x0a, 0x1c, 0x52, 0x65, + 0x73, 0x75, 0x6d, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x79, + 0x6e, 0x63, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x44, 0x2e, 0x74, 0x72, 0x70, + 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x63, 0x68, 0x65, 0x64, + 0x75, 0x6c, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x39, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x63, 0x68, 0x65, 0x64, + 0x75, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x8b, 0x01, 0x0a, 0x18, + 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3a, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x8b, 0x01, 0x0a, 0x18, 0x52, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x47, 0x50, 0x47, 0x4b, 0x65, 0x79, 0x12, 0x3a, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x47, 0x50, 0x47, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x47, 0x50, 0x47, 0x4b, 0x65, 0x79, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x8a, 0x01, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x47, 0x50, 0x47, 0x4b, 0x65, 0x79, + 0x73, 0x12, 0x37, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x4c, 0x69, + 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x47, 0x50, 0x47, 0x4b, + 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x38, 0x2e, 0x74, 0x72, 0x70, + 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x47, 0x50, 0x47, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6a, 0x0a, 0x16, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x47, 0x50, 0x47, 0x4b, 0x65, 0x79, 0x12, 0x38, + 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, + 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x47, 0x50, 0x47, 0x4b, 0x65, + 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x12, 0x85, 0x01, 0x0a, 0x16, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x12, 0x38, 0x2e, 0x74, 0x72, + 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x84, 0x01, 0x0a, 0x13, 0x4c, 0x69, 0x73, + 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x73, + 0x12, 0x35, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, + 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x36, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, + 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x81, 0x01, 0x0a, 0x14, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x12, 0x36, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x31, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x7f, 0x0a, 0x13, 0x52, 0x65, 0x74, 0x72, 0x79, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x12, 0x35, 0x2e, 0x74, 0x72, 0x70, + 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x74, 0x72, 0x79, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x31, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x9e, 0x01, 0x0a, 0x1d, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x44, 0x69, 0x61, + 0x67, 0x6e, 0x6f, 0x73, 0x69, 0x73, 0x12, 0x3f, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x2e, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x69, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3c, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, + 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x44, 0x69, + 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x69, 0x73, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x8e, 0x01, 0x0a, 0x19, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, + 0x62, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, + 0x4c, 0x6f, 0x67, 0x12, 0x3b, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, + 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x34, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x4c, 0x6f, 0x67, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x70, 0x0a, 0x19, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, + 0x61, 0x64, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, + 0x4c, 0x6f, 0x67, 0x12, 0x3b, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, + 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x94, 0x01, 0x0a, 0x1a, 0x42, 0x75, 0x69, + 0x6c, 0x64, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x43, 0x41, 0x49, + 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x3c, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, + 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x4f, 0x43, 0x41, 0x49, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x38, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x43, 0x41, 0x49, + 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x99, 0x01, 0x0a, 0x1c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x43, 0x41, 0x49, 0x41, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, 0x73, + 0x12, 0x3e, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x43, 0x41, + 0x49, 0x41, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x39, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x43, 0x41, 0x49, 0x41, 0x6e, 0x61, 0x6c, 0x79, + 0x73, 0x69, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x9d, 0x01, 0x0a, 0x1e, + 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x4f, 0x43, 0x41, 0x49, 0x41, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, 0x73, 0x12, 0x40, + 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, + 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x44, 0x65, 0x73, 0x63, 0x72, + 0x69, 0x62, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x43, 0x41, + 0x49, 0x41, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x39, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x43, 0x41, 0x49, 0x41, 0x6e, 0x61, 0x6c, 0x79, + 0x73, 0x69, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x99, 0x01, 0x0a, 0x1c, + 0x41, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x4f, 0x43, 0x41, 0x49, 0x46, 0x65, 0x65, 0x64, 0x62, 0x61, 0x63, 0x6b, 0x12, 0x3e, 0x2e, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4f, 0x43, 0x41, 0x49, 0x46, 0x65, 0x65, + 0x64, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x39, 0x2e, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x4f, 0x43, 0x41, 0x49, 0x41, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x85, 0x01, 0x0a, 0x15, 0x42, 0x65, 0x67, 0x69, + 0x6e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, 0x61, + 0x64, 0x12, 0x37, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x42, 0x65, + 0x67, 0x69, 0x6e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x6c, + 0x6f, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x74, 0x72, 0x70, + 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x91, 0x01, 0x0a, 0x1b, 0x41, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x12, + 0x3d, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, + 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x65, + 0x6e, 0x64, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, + 0x61, 0x64, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, + 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, + 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x93, 0x01, 0x0a, 0x18, 0x50, 0x75, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x43, 0x68, 0x75, 0x6e, 0x6b, + 0x12, 0x3a, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x50, 0x75, 0x74, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, + 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3b, 0x2e, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x50, 0x75, 0x74, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x43, 0x68, 0x75, 0x6e, + 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x9f, 0x01, 0x0a, 0x1e, 0x44, 0x65, + 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x73, 0x12, 0x3a, 0x2e, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, + 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, 0x61, + 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x41, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x43, 0x68, 0x75, + 0x6e, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x88, 0x01, 0x0a, 0x15, + 0x50, 0x61, 0x75, 0x73, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, + 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x3a, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x2e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x33, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x89, 0x01, 0x0a, 0x16, 0x52, 0x65, 0x73, 0x75, 0x6d, + 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, 0x61, + 0x64, 0x12, 0x3a, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x44, 0x65, + 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x93, 0x01, 0x0a, 0x18, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x12, + 0x3a, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, + 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x43, 0x6f, 0x6d, 0x70, + 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, + 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3b, 0x2e, 0x74, 0x72, + 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x85, 0x01, 0x0a, 0x15, 0x41, 0x62, 0x6f, + 0x72, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, + 0x61, 0x64, 0x12, 0x37, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x41, + 0x62, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, + 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x74, 0x72, + 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x8b, 0x01, 0x0a, 0x18, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x3a, 0x2e, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, + 0x62, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x70, 0x6c, 0x6f, + 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x8d, + 0x01, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x12, 0x38, 0x2e, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x39, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, + 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x93, + 0x01, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x51, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x69, 0x6e, 0x65, 0x12, 0x3a, 0x2e, 0x74, 0x72, + 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x51, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x69, 0x6e, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3b, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, + 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x51, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x87, 0x01, 0x0a, 0x17, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, + 0x12, 0x39, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, 0x63, + 0x6b, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x74, 0x72, + 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xab, + 0x01, 0x0a, 0x20, 0x50, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x42, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, + 0x50, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x43, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, + 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, + 0x63, 0x6b, 0x61, 0x67, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x65, + 0x76, 0x69, 0x65, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x99, 0x01, 0x0a, + 0x20, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x42, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, + 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xa8, 0x01, 0x0a, 0x1f, 0x4c, 0x69, 0x73, + 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x44, 0x69, 0x73, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x6f, 0x64, 0x65, 0x73, 0x12, 0x41, 0x2e, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x69, 0x6f, 0x6e, 0x4e, 0x6f, 0x64, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x42, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, + 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x6f, 0x64, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x99, 0x01, 0x0a, 0x1c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3e, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x39, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x44, 0x69, 0x73, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x9d, 0x01, 0x0a, 0x1e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x40, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x44, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x39, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x44, 0x69, 0x73, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x9c, 0x01, 0x0a, 0x1b, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, + 0x3d, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, + 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3e, + 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, + 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xa8, + 0x01, 0x0a, 0x1f, 0x50, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, + 0x6c, 0x6c, 0x12, 0x41, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x50, + 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x42, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, 0x63, 0x6b, + 0x61, 0x67, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x50, 0x72, 0x65, 0x76, 0x69, 0x65, + 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xa1, 0x01, 0x0a, 0x1f, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, + 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x12, 0x41, 0x2e, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x72, + 0x6d, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, 0x63, 0x6b, 0x61, + 0x67, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x3b, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x49, 0x6e, + 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xa3, 0x01, + 0x0a, 0x20, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, + 0x6c, 0x6c, 0x12, 0x42, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x44, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3b, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, 0x63, + 0x6b, 0x61, 0x67, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x8d, 0x01, 0x0a, 0x19, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x6e, 0x76, 0x65, 0x6e, 0x74, 0x6f, 0x72, + 0x79, 0x12, 0x3b, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x45, 0x78, + 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x6e, + 0x76, 0x65, 0x6e, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, + 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, + 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0xab, 0x01, 0x0a, 0x20, 0x50, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x6e, 0x76, 0x65, 0x6e, 0x74, 0x6f, + 0x72, 0x79, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x42, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x50, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x6e, 0x76, 0x65, 0x6e, 0x74, 0x6f, 0x72, 0x79, 0x49, + 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x43, 0x2e, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x49, 0x6e, 0x76, 0x65, 0x6e, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x6d, 0x70, 0x6f, + 0x72, 0x74, 0x50, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0xa4, 0x01, 0x0a, 0x20, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x6e, 0x76, 0x65, 0x6e, 0x74, 0x6f, 0x72, 0x79, + 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x42, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x49, 0x6e, 0x76, 0x65, 0x6e, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x6d, 0x70, + 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3c, 0x2e, 0x74, 0x72, 0x70, + 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x49, 0x6e, 0x76, 0x65, 0x6e, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x9b, 0x01, 0x0a, 0x20, 0x45, 0x78, 0x70, + 0x6f, 0x72, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x56, 0x61, 0x6c, + 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x42, 0x2e, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x33, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x9f, 0x01, 0x0a, 0x22, 0x45, 0x78, 0x70, 0x6f, 0x72, + 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x44, 0x69, 0x73, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x44, 0x2e, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7b, 0x0a, 0x11, 0x4d, 0x69, 0x67, 0x72, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x33, 0x2e, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, + 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x45, 0x5a, 0x43, 0x67, 0x69, 0x74, 0x65, 0x65, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x4f, 0x70, 0x65, 0x6e, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x4f, 0x53, 0x2f, 0x6f, + 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, + 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, + 0x6c, 0x2f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_repository_repository_proto_rawDescOnce sync.Once + file_repository_repository_proto_rawDescData = file_repository_repository_proto_rawDesc +) + +func file_repository_repository_proto_rawDescGZIP() []byte { + file_repository_repository_proto_rawDescOnce.Do(func() { + file_repository_repository_proto_rawDescData = protoimpl.X.CompressGZIP(file_repository_repository_proto_rawDescData) + }) + return file_repository_repository_proto_rawDescData +} + +var file_repository_repository_proto_enumTypes = make([]protoimpl.EnumInfo, 11) +var file_repository_repository_proto_msgTypes = make([]protoimpl.MessageInfo, 163) +var file_repository_repository_proto_goTypes = []interface{}{ + (RepositoryType)(0), // 0: trpc.ocmanager.repository.RepositoryType + (RepositoryArchitecture)(0), // 1: trpc.ocmanager.repository.RepositoryArchitecture + (RepositoryGPGPolicy)(0), // 2: trpc.ocmanager.repository.RepositoryGPGPolicy + (RepositorySyncMode)(0), // 3: trpc.ocmanager.repository.RepositorySyncMode + (RepositoryInventoryConflictStrategy)(0), // 4: trpc.ocmanager.repository.RepositoryInventoryConflictStrategy + (RepositorySyncScheduleCadence)(0), // 5: trpc.ocmanager.repository.RepositorySyncScheduleCadence + (RepositorySyncScheduleStatus)(0), // 6: trpc.ocmanager.repository.RepositorySyncScheduleStatus + (RepositoryTaskType)(0), // 7: trpc.ocmanager.repository.RepositoryTaskType + (RepositoryTaskStatus)(0), // 8: trpc.ocmanager.repository.RepositoryTaskStatus + (RepositoryStorageNodeStatus)(0), // 9: trpc.ocmanager.repository.RepositoryStorageNodeStatus + (RepositoryNodeEnrollmentStatus)(0), // 10: trpc.ocmanager.repository.RepositoryNodeEnrollmentStatus + (*RepositorySpec)(nil), // 11: trpc.ocmanager.repository.RepositorySpec + (*Repository)(nil), // 12: trpc.ocmanager.repository.Repository + (*CreateRepositoryRequest)(nil), // 13: trpc.ocmanager.repository.CreateRepositoryRequest + (*UpdateRepositoryRequest)(nil), // 14: trpc.ocmanager.repository.UpdateRepositoryRequest + (*DescribeRepositoryRequest)(nil), // 15: trpc.ocmanager.repository.DescribeRepositoryRequest + (*ListRepositoriesRequest)(nil), // 16: trpc.ocmanager.repository.ListRepositoriesRequest + (*DeleteRepositoryRequest)(nil), // 17: trpc.ocmanager.repository.DeleteRepositoryRequest + (*BatchApplyRepositoryItem)(nil), // 18: trpc.ocmanager.repository.BatchApplyRepositoryItem + (*BatchApplyRepositoriesRequest)(nil), // 19: trpc.ocmanager.repository.BatchApplyRepositoriesRequest + (*BatchApplyRepositoryResult)(nil), // 20: trpc.ocmanager.repository.BatchApplyRepositoryResult + (*RepositoryResponse)(nil), // 21: trpc.ocmanager.repository.RepositoryResponse + (*ListRepositoriesResponse)(nil), // 22: trpc.ocmanager.repository.ListRepositoriesResponse + (*BatchApplyRepositoriesResponse)(nil), // 23: trpc.ocmanager.repository.BatchApplyRepositoriesResponse + (*RepositoryStorageNodeSpec)(nil), // 24: trpc.ocmanager.repository.RepositoryStorageNodeSpec + (*RepositoryStorageNode)(nil), // 25: trpc.ocmanager.repository.RepositoryStorageNode + (*RepositoryNodeRepositorySummary)(nil), // 26: trpc.ocmanager.repository.RepositoryNodeRepositorySummary + (*CreateRepositoryStorageNodeRequest)(nil), // 27: trpc.ocmanager.repository.CreateRepositoryStorageNodeRequest + (*UpdateRepositoryStorageNodeRequest)(nil), // 28: trpc.ocmanager.repository.UpdateRepositoryStorageNodeRequest + (*DescribeRepositoryStorageNodeRequest)(nil), // 29: trpc.ocmanager.repository.DescribeRepositoryStorageNodeRequest + (*ListRepositoryStorageNodesRequest)(nil), // 30: trpc.ocmanager.repository.ListRepositoryStorageNodesRequest + (*DeleteRepositoryStorageNodeRequest)(nil), // 31: trpc.ocmanager.repository.DeleteRepositoryStorageNodeRequest + (*ProbeRepositoryStorageNodeRequest)(nil), // 32: trpc.ocmanager.repository.ProbeRepositoryStorageNodeRequest + (*RepositoryStorageNodeResponse)(nil), // 33: trpc.ocmanager.repository.RepositoryStorageNodeResponse + (*RepositoryMetadataSigningKey)(nil), // 34: trpc.ocmanager.repository.RepositoryMetadataSigningKey + (*ListRepositoryStorageNodesResponse)(nil), // 35: trpc.ocmanager.repository.ListRepositoryStorageNodesResponse + (*RepositoryNodeEnrollmentSpec)(nil), // 36: trpc.ocmanager.repository.RepositoryNodeEnrollmentSpec + (*RepositoryNodeEnrollment)(nil), // 37: trpc.ocmanager.repository.RepositoryNodeEnrollment + (*CreateRepositoryNodeEnrollmentRequest)(nil), // 38: trpc.ocmanager.repository.CreateRepositoryNodeEnrollmentRequest + (*RepositoryNodeEnrollmentResponse)(nil), // 39: trpc.ocmanager.repository.RepositoryNodeEnrollmentResponse + (*RepositoryNodeArtifactSpec)(nil), // 40: trpc.ocmanager.repository.RepositoryNodeArtifactSpec + (*RepositoryNodeArtifact)(nil), // 41: trpc.ocmanager.repository.RepositoryNodeArtifact + (*RegisterRepositoryNodeArtifactRequest)(nil), // 42: trpc.ocmanager.repository.RegisterRepositoryNodeArtifactRequest + (*RepositoryNodeArtifactResponse)(nil), // 43: trpc.ocmanager.repository.RepositoryNodeArtifactResponse + (*ListRepositoryNodeArtifactsRequest)(nil), // 44: trpc.ocmanager.repository.ListRepositoryNodeArtifactsRequest + (*ListRepositoryNodeArtifactsResponse)(nil), // 45: trpc.ocmanager.repository.ListRepositoryNodeArtifactsResponse + (*DownloadRepositoryNodeArtifactRequest)(nil), // 46: trpc.ocmanager.repository.DownloadRepositoryNodeArtifactRequest + (*RepositoryNodeInstallSourceSpec)(nil), // 47: trpc.ocmanager.repository.RepositoryNodeInstallSourceSpec + (*RepositoryNodeInstallSource)(nil), // 48: trpc.ocmanager.repository.RepositoryNodeInstallSource + (*ConfigureRepositoryNodeInstallSourceRequest)(nil), // 49: trpc.ocmanager.repository.ConfigureRepositoryNodeInstallSourceRequest + (*RepositoryNodeInstallSourceResponse)(nil), // 50: trpc.ocmanager.repository.RepositoryNodeInstallSourceResponse + (*ListRepositoryNodeInstallSourcesRequest)(nil), // 51: trpc.ocmanager.repository.ListRepositoryNodeInstallSourcesRequest + (*ListRepositoryNodeInstallSourcesResponse)(nil), // 52: trpc.ocmanager.repository.ListRepositoryNodeInstallSourcesResponse + (*RepositoryNodeRegistrationAllowlistSpec)(nil), // 53: trpc.ocmanager.repository.RepositoryNodeRegistrationAllowlistSpec + (*RepositoryNodeRegistrationAllowlist)(nil), // 54: trpc.ocmanager.repository.RepositoryNodeRegistrationAllowlist + (*UpsertRepositoryNodeRegistrationAllowlistRequest)(nil), // 55: trpc.ocmanager.repository.UpsertRepositoryNodeRegistrationAllowlistRequest + (*RepositoryNodeRegistrationAllowlistResponse)(nil), // 56: trpc.ocmanager.repository.RepositoryNodeRegistrationAllowlistResponse + (*ListRepositoryNodeRegistrationAllowlistsRequest)(nil), // 57: trpc.ocmanager.repository.ListRepositoryNodeRegistrationAllowlistsRequest + (*ListRepositoryNodeRegistrationAllowlistsResponse)(nil), // 58: trpc.ocmanager.repository.ListRepositoryNodeRegistrationAllowlistsResponse + (*RepositoryPreflightRecord)(nil), // 59: trpc.ocmanager.repository.RepositoryPreflightRecord + (*RepositoryStorageNodeProbeResponse)(nil), // 60: trpc.ocmanager.repository.RepositoryStorageNodeProbeResponse + (*ListRepositoryPreflightRecordsRequest)(nil), // 61: trpc.ocmanager.repository.ListRepositoryPreflightRecordsRequest + (*ListRepositoryPreflightRecordsResponse)(nil), // 62: trpc.ocmanager.repository.ListRepositoryPreflightRecordsResponse + (*SyncRepositoryRequest)(nil), // 63: trpc.ocmanager.repository.SyncRepositoryRequest + (*RepositorySyncScheduleSpec)(nil), // 64: trpc.ocmanager.repository.RepositorySyncScheduleSpec + (*RepositorySyncSchedule)(nil), // 65: trpc.ocmanager.repository.RepositorySyncSchedule + (*UpsertRepositorySyncScheduleRequest)(nil), // 66: trpc.ocmanager.repository.UpsertRepositorySyncScheduleRequest + (*DescribeRepositorySyncScheduleRequest)(nil), // 67: trpc.ocmanager.repository.DescribeRepositorySyncScheduleRequest + (*ChangeRepositorySyncScheduleStatusRequest)(nil), // 68: trpc.ocmanager.repository.ChangeRepositorySyncScheduleStatusRequest + (*RepositorySyncScheduleResponse)(nil), // 69: trpc.ocmanager.repository.RepositorySyncScheduleResponse + (*RepositoryTask)(nil), // 70: trpc.ocmanager.repository.RepositoryTask + (*RepositoryTaskResultItem)(nil), // 71: trpc.ocmanager.repository.RepositoryTaskResultItem + (*RepositoryTaskTarget)(nil), // 72: trpc.ocmanager.repository.RepositoryTaskTarget + (*DescribeRepositoryTaskRequest)(nil), // 73: trpc.ocmanager.repository.DescribeRepositoryTaskRequest + (*ListRepositoryTasksRequest)(nil), // 74: trpc.ocmanager.repository.ListRepositoryTasksRequest + (*RepositoryTaskStatistics)(nil), // 75: trpc.ocmanager.repository.RepositoryTaskStatistics + (*CancelRepositoryTaskRequest)(nil), // 76: trpc.ocmanager.repository.CancelRepositoryTaskRequest + (*RetryRepositoryTaskRequest)(nil), // 77: trpc.ocmanager.repository.RetryRepositoryTaskRequest + (*RepositoryTaskResponse)(nil), // 78: trpc.ocmanager.repository.RepositoryTaskResponse + (*RepositoryOCAIContext)(nil), // 79: trpc.ocmanager.repository.RepositoryOCAIContext + (*RepositoryOCAIAnalysis)(nil), // 80: trpc.ocmanager.repository.RepositoryOCAIAnalysis + (*BuildRepositoryOCAIContextRequest)(nil), // 81: trpc.ocmanager.repository.BuildRepositoryOCAIContextRequest + (*CreateRepositoryOCAIAnalysisRequest)(nil), // 82: trpc.ocmanager.repository.CreateRepositoryOCAIAnalysisRequest + (*DescribeRepositoryOCAIAnalysisRequest)(nil), // 83: trpc.ocmanager.repository.DescribeRepositoryOCAIAnalysisRequest + (*AppendRepositoryOCAIFeedbackRequest)(nil), // 84: trpc.ocmanager.repository.AppendRepositoryOCAIFeedbackRequest + (*RepositoryOCAIContextResponse)(nil), // 85: trpc.ocmanager.repository.RepositoryOCAIContextResponse + (*RepositoryOCAIAnalysisResponse)(nil), // 86: trpc.ocmanager.repository.RepositoryOCAIAnalysisResponse + (*GenerateRepositoryConfigRequest)(nil), // 87: trpc.ocmanager.repository.GenerateRepositoryConfigRequest + (*RepositoryConfig)(nil), // 88: trpc.ocmanager.repository.RepositoryConfig + (*RepositoryConfigResponse)(nil), // 89: trpc.ocmanager.repository.RepositoryConfigResponse + (*RepositoryGPGKey)(nil), // 90: trpc.ocmanager.repository.RepositoryGPGKey + (*RegisterRepositoryGPGKeyRequest)(nil), // 91: trpc.ocmanager.repository.RegisterRepositoryGPGKeyRequest + (*ListRepositoryGPGKeysRequest)(nil), // 92: trpc.ocmanager.repository.ListRepositoryGPGKeysRequest + (*DeleteRepositoryGPGKeyRequest)(nil), // 93: trpc.ocmanager.repository.DeleteRepositoryGPGKeyRequest + (*RepositoryGPGKeyResponse)(nil), // 94: trpc.ocmanager.repository.RepositoryGPGKeyResponse + (*ListRepositoryGPGKeysResponse)(nil), // 95: trpc.ocmanager.repository.ListRepositoryGPGKeysResponse + (*ListRepositoryTasksResponse)(nil), // 96: trpc.ocmanager.repository.ListRepositoryTasksResponse + (*RepositoryUpload)(nil), // 97: trpc.ocmanager.repository.RepositoryUpload + (*BeginRepositoryUploadRequest)(nil), // 98: trpc.ocmanager.repository.BeginRepositoryUploadRequest + (*AppendRepositoryUploadChunkRequest)(nil), // 99: trpc.ocmanager.repository.AppendRepositoryUploadChunkRequest + (*PutRepositoryUploadChunkRequest)(nil), // 100: trpc.ocmanager.repository.PutRepositoryUploadChunkRequest + (*RepositoryUploadChunk)(nil), // 101: trpc.ocmanager.repository.RepositoryUploadChunk + (*PutRepositoryUploadChunkResponse)(nil), // 102: trpc.ocmanager.repository.PutRepositoryUploadChunkResponse + (*DescribeRepositoryUploadChunksResponse)(nil), // 103: trpc.ocmanager.repository.DescribeRepositoryUploadChunksResponse + (*CompleteRepositoryUploadRequest)(nil), // 104: trpc.ocmanager.repository.CompleteRepositoryUploadRequest + (*AbortRepositoryUploadRequest)(nil), // 105: trpc.ocmanager.repository.AbortRepositoryUploadRequest + (*DescribeRepositoryUploadRequest)(nil), // 106: trpc.ocmanager.repository.DescribeRepositoryUploadRequest + (*RepositoryUploadResponse)(nil), // 107: trpc.ocmanager.repository.RepositoryUploadResponse + (*CompleteRepositoryUploadResponse)(nil), // 108: trpc.ocmanager.repository.CompleteRepositoryUploadResponse + (*RepositoryPackage)(nil), // 109: trpc.ocmanager.repository.RepositoryPackage + (*ListRepositoryPackagesRequest)(nil), // 110: trpc.ocmanager.repository.ListRepositoryPackagesRequest + (*ListRepositoryPackagesResponse)(nil), // 111: trpc.ocmanager.repository.ListRepositoryPackagesResponse + (*RepositoryQuarantineItem)(nil), // 112: trpc.ocmanager.repository.RepositoryQuarantineItem + (*ListRepositoryQuarantineRequest)(nil), // 113: trpc.ocmanager.repository.ListRepositoryQuarantineRequest + (*ListRepositoryQuarantineResponse)(nil), // 114: trpc.ocmanager.repository.ListRepositoryQuarantineResponse + (*DeleteRepositoryPackageRequest)(nil), // 115: trpc.ocmanager.repository.DeleteRepositoryPackageRequest + (*RepositoryDiagnosis)(nil), // 116: trpc.ocmanager.repository.RepositoryDiagnosis + (*ExportRepositoryTaskDiagnosisRequest)(nil), // 117: trpc.ocmanager.repository.ExportRepositoryTaskDiagnosisRequest + (*RepositoryDiagnosisReportResponse)(nil), // 118: trpc.ocmanager.repository.RepositoryDiagnosisReportResponse + (*RepositoryDistributionNode)(nil), // 119: trpc.ocmanager.repository.RepositoryDistributionNode + (*ListRepositoryDistributionNodesRequest)(nil), // 120: trpc.ocmanager.repository.ListRepositoryDistributionNodesRequest + (*ListRepositoryDistributionNodesResponse)(nil), // 121: trpc.ocmanager.repository.ListRepositoryDistributionNodesResponse + (*CreateRepositoryDistributionRequest)(nil), // 122: trpc.ocmanager.repository.CreateRepositoryDistributionRequest + (*DescribeRepositoryDistributionRequest)(nil), // 123: trpc.ocmanager.repository.DescribeRepositoryDistributionRequest + (*ListRepositoryDistributionsRequest)(nil), // 124: trpc.ocmanager.repository.ListRepositoryDistributionsRequest + (*RepositoryDistributionTarget)(nil), // 125: trpc.ocmanager.repository.RepositoryDistributionTarget + (*RepositoryDistribution)(nil), // 126: trpc.ocmanager.repository.RepositoryDistribution + (*RepositoryDistributionResponse)(nil), // 127: trpc.ocmanager.repository.RepositoryDistributionResponse + (*ListRepositoryDistributionsResponse)(nil), // 128: trpc.ocmanager.repository.ListRepositoryDistributionsResponse + (*PreviewRepositoryPackageDeletionRequest)(nil), // 129: trpc.ocmanager.repository.PreviewRepositoryPackageDeletionRequest + (*ConfirmRepositoryPackageDeletionRequest)(nil), // 130: trpc.ocmanager.repository.ConfirmRepositoryPackageDeletionRequest + (*RepositoryPackageDeletionPreviewSource)(nil), // 131: trpc.ocmanager.repository.RepositoryPackageDeletionPreviewSource + (*RepositoryPackageDependencyImpact)(nil), // 132: trpc.ocmanager.repository.RepositoryPackageDependencyImpact + (*RepositoryPackageDeletionPreview)(nil), // 133: trpc.ocmanager.repository.RepositoryPackageDeletionPreview + (*RepositoryPackageDeletionPreviewResponse)(nil), // 134: trpc.ocmanager.repository.RepositoryPackageDeletionPreviewResponse + (*PreviewRepositoryPackageInstallRequest)(nil), // 135: trpc.ocmanager.repository.PreviewRepositoryPackageInstallRequest + (*RepositoryPackageInstallPreviewTarget)(nil), // 136: trpc.ocmanager.repository.RepositoryPackageInstallPreviewTarget + (*RepositoryPackageInstallPreview)(nil), // 137: trpc.ocmanager.repository.RepositoryPackageInstallPreview + (*RepositoryPackageInstallPreviewResponse)(nil), // 138: trpc.ocmanager.repository.RepositoryPackageInstallPreviewResponse + (*ConfirmRepositoryPackageInstallRequest)(nil), // 139: trpc.ocmanager.repository.ConfirmRepositoryPackageInstallRequest + (*DescribeRepositoryPackageInstallRequest)(nil), // 140: trpc.ocmanager.repository.DescribeRepositoryPackageInstallRequest + (*RepositoryPackageInstall)(nil), // 141: trpc.ocmanager.repository.RepositoryPackageInstall + (*RepositoryPackageInstallResponse)(nil), // 142: trpc.ocmanager.repository.RepositoryPackageInstallResponse + (*ExportRepositoryInventoryRequest)(nil), // 143: trpc.ocmanager.repository.ExportRepositoryInventoryRequest + (*ExportRepositoryValidationReportRequest)(nil), // 144: trpc.ocmanager.repository.ExportRepositoryValidationReportRequest + (*ExportRepositoryDistributionResultRequest)(nil), // 145: trpc.ocmanager.repository.ExportRepositoryDistributionResultRequest + (*RepositoryExportResponse)(nil), // 146: trpc.ocmanager.repository.RepositoryExportResponse + (*RepositoryInventoryStorageNodeMapping)(nil), // 147: trpc.ocmanager.repository.RepositoryInventoryStorageNodeMapping + (*RepositoryInventorySigningKeyMapping)(nil), // 148: trpc.ocmanager.repository.RepositoryInventorySigningKeyMapping + (*PreviewRepositoryInventoryImportRequest)(nil), // 149: trpc.ocmanager.repository.PreviewRepositoryInventoryImportRequest + (*RepositoryInventoryImportItem)(nil), // 150: trpc.ocmanager.repository.RepositoryInventoryImportItem + (*RepositoryInventoryImportPreviewResponse)(nil), // 151: trpc.ocmanager.repository.RepositoryInventoryImportPreviewResponse + (*ConfirmRepositoryInventoryImportRequest)(nil), // 152: trpc.ocmanager.repository.ConfirmRepositoryInventoryImportRequest + (*RepositoryInventoryImportJob)(nil), // 153: trpc.ocmanager.repository.RepositoryInventoryImportJob + (*RepositoryInventoryImportResponse)(nil), // 154: trpc.ocmanager.repository.RepositoryInventoryImportResponse + (*MigrateRepositoryRequest)(nil), // 155: trpc.ocmanager.repository.MigrateRepositoryRequest + (*DescribeRepositoryTaskLogRequest)(nil), // 156: trpc.ocmanager.repository.DescribeRepositoryTaskLogRequest + (*DownloadRepositoryTaskLogRequest)(nil), // 157: trpc.ocmanager.repository.DownloadRepositoryTaskLogRequest + (*RepositoryTaskLog)(nil), // 158: trpc.ocmanager.repository.RepositoryTaskLog + (*RepositoryTaskLogResponse)(nil), // 159: trpc.ocmanager.repository.RepositoryTaskLogResponse + (*RepositoryUpstreamProfileSpec)(nil), // 160: trpc.ocmanager.repository.RepositoryUpstreamProfileSpec + (*RepositoryUpstreamProfile)(nil), // 161: trpc.ocmanager.repository.RepositoryUpstreamProfile + (*CreateRepositoryUpstreamProfileRequest)(nil), // 162: trpc.ocmanager.repository.CreateRepositoryUpstreamProfileRequest + (*UpdateRepositoryUpstreamProfileRequest)(nil), // 163: trpc.ocmanager.repository.UpdateRepositoryUpstreamProfileRequest + (*DescribeRepositoryUpstreamProfileRequest)(nil), // 164: trpc.ocmanager.repository.DescribeRepositoryUpstreamProfileRequest + (*ListRepositoryUpstreamProfilesRequest)(nil), // 165: trpc.ocmanager.repository.ListRepositoryUpstreamProfilesRequest + (*DeleteRepositoryUpstreamProfileRequest)(nil), // 166: trpc.ocmanager.repository.DeleteRepositoryUpstreamProfileRequest + (*RepositoryUpstreamProfileResponse)(nil), // 167: trpc.ocmanager.repository.RepositoryUpstreamProfileResponse + (*ListRepositoryUpstreamProfilesResponse)(nil), // 168: trpc.ocmanager.repository.ListRepositoryUpstreamProfilesResponse + nil, // 169: trpc.ocmanager.repository.RepositoryStorageNodeSpec.LabelsEntry + nil, // 170: trpc.ocmanager.repository.RepositoryStorageNode.ToolVersionsEntry + nil, // 171: trpc.ocmanager.repository.RepositoryStorageNode.LabelsEntry + nil, // 172: trpc.ocmanager.repository.RepositoryNodeEnrollmentSpec.LabelsEntry + nil, // 173: trpc.ocmanager.repository.RepositoryPreflightRecord.ToolVersionsEntry + (*timestamppb.Timestamp)(nil), // 174: google.protobuf.Timestamp + (*common.PageRequest)(nil), // 175: trpc.ocmanager.common.PageRequest + (*common.PageResponse)(nil), // 176: trpc.ocmanager.common.PageResponse + (*emptypb.Empty)(nil), // 177: google.protobuf.Empty +} +var file_repository_repository_proto_depIdxs = []int32{ + 1, // 0: trpc.ocmanager.repository.RepositorySpec.architecture:type_name -> trpc.ocmanager.repository.RepositoryArchitecture + 0, // 1: trpc.ocmanager.repository.RepositorySpec.type:type_name -> trpc.ocmanager.repository.RepositoryType + 2, // 2: trpc.ocmanager.repository.RepositorySpec.gpg_policy:type_name -> trpc.ocmanager.repository.RepositoryGPGPolicy + 3, // 3: trpc.ocmanager.repository.RepositorySpec.sync_mode:type_name -> trpc.ocmanager.repository.RepositorySyncMode + 1, // 4: trpc.ocmanager.repository.Repository.architecture:type_name -> trpc.ocmanager.repository.RepositoryArchitecture + 0, // 5: trpc.ocmanager.repository.Repository.type:type_name -> trpc.ocmanager.repository.RepositoryType + 2, // 6: trpc.ocmanager.repository.Repository.gpg_policy:type_name -> trpc.ocmanager.repository.RepositoryGPGPolicy + 174, // 7: trpc.ocmanager.repository.Repository.created_at:type_name -> google.protobuf.Timestamp + 174, // 8: trpc.ocmanager.repository.Repository.updated_at:type_name -> google.protobuf.Timestamp + 174, // 9: trpc.ocmanager.repository.Repository.last_sync_at:type_name -> google.protobuf.Timestamp + 8, // 10: trpc.ocmanager.repository.Repository.last_sync_status:type_name -> trpc.ocmanager.repository.RepositoryTaskStatus + 3, // 11: trpc.ocmanager.repository.Repository.sync_mode:type_name -> trpc.ocmanager.repository.RepositorySyncMode + 11, // 12: trpc.ocmanager.repository.CreateRepositoryRequest.spec:type_name -> trpc.ocmanager.repository.RepositorySpec + 11, // 13: trpc.ocmanager.repository.UpdateRepositoryRequest.spec:type_name -> trpc.ocmanager.repository.RepositorySpec + 1, // 14: trpc.ocmanager.repository.ListRepositoriesRequest.architecture:type_name -> trpc.ocmanager.repository.RepositoryArchitecture + 0, // 15: trpc.ocmanager.repository.ListRepositoriesRequest.type:type_name -> trpc.ocmanager.repository.RepositoryType + 175, // 16: trpc.ocmanager.repository.ListRepositoriesRequest.page_request:type_name -> trpc.ocmanager.common.PageRequest + 11, // 17: trpc.ocmanager.repository.BatchApplyRepositoryItem.spec:type_name -> trpc.ocmanager.repository.RepositorySpec + 18, // 18: trpc.ocmanager.repository.BatchApplyRepositoriesRequest.items:type_name -> trpc.ocmanager.repository.BatchApplyRepositoryItem + 12, // 19: trpc.ocmanager.repository.BatchApplyRepositoryResult.repository:type_name -> trpc.ocmanager.repository.Repository + 12, // 20: trpc.ocmanager.repository.RepositoryResponse.repository:type_name -> trpc.ocmanager.repository.Repository + 12, // 21: trpc.ocmanager.repository.ListRepositoriesResponse.repositories:type_name -> trpc.ocmanager.repository.Repository + 176, // 22: trpc.ocmanager.repository.ListRepositoriesResponse.page_response:type_name -> trpc.ocmanager.common.PageResponse + 20, // 23: trpc.ocmanager.repository.BatchApplyRepositoriesResponse.results:type_name -> trpc.ocmanager.repository.BatchApplyRepositoryResult + 169, // 24: trpc.ocmanager.repository.RepositoryStorageNodeSpec.labels:type_name -> trpc.ocmanager.repository.RepositoryStorageNodeSpec.LabelsEntry + 9, // 25: trpc.ocmanager.repository.RepositoryStorageNode.status:type_name -> trpc.ocmanager.repository.RepositoryStorageNodeStatus + 170, // 26: trpc.ocmanager.repository.RepositoryStorageNode.tool_versions:type_name -> trpc.ocmanager.repository.RepositoryStorageNode.ToolVersionsEntry + 171, // 27: trpc.ocmanager.repository.RepositoryStorageNode.labels:type_name -> trpc.ocmanager.repository.RepositoryStorageNode.LabelsEntry + 174, // 28: trpc.ocmanager.repository.RepositoryStorageNode.last_heartbeat_at:type_name -> google.protobuf.Timestamp + 174, // 29: trpc.ocmanager.repository.RepositoryStorageNode.last_probe_at:type_name -> google.protobuf.Timestamp + 174, // 30: trpc.ocmanager.repository.RepositoryStorageNode.created_at:type_name -> google.protobuf.Timestamp + 174, // 31: trpc.ocmanager.repository.RepositoryStorageNode.updated_at:type_name -> google.protobuf.Timestamp + 0, // 32: trpc.ocmanager.repository.RepositoryNodeRepositorySummary.repository_type:type_name -> trpc.ocmanager.repository.RepositoryType + 1, // 33: trpc.ocmanager.repository.RepositoryNodeRepositorySummary.architecture:type_name -> trpc.ocmanager.repository.RepositoryArchitecture + 174, // 34: trpc.ocmanager.repository.RepositoryNodeRepositorySummary.last_sync_at:type_name -> google.protobuf.Timestamp + 8, // 35: trpc.ocmanager.repository.RepositoryNodeRepositorySummary.last_sync_status:type_name -> trpc.ocmanager.repository.RepositoryTaskStatus + 174, // 36: trpc.ocmanager.repository.RepositoryNodeRepositorySummary.next_sync_at:type_name -> google.protobuf.Timestamp + 24, // 37: trpc.ocmanager.repository.CreateRepositoryStorageNodeRequest.spec:type_name -> trpc.ocmanager.repository.RepositoryStorageNodeSpec + 24, // 38: trpc.ocmanager.repository.UpdateRepositoryStorageNodeRequest.spec:type_name -> trpc.ocmanager.repository.RepositoryStorageNodeSpec + 9, // 39: trpc.ocmanager.repository.ListRepositoryStorageNodesRequest.status:type_name -> trpc.ocmanager.repository.RepositoryStorageNodeStatus + 175, // 40: trpc.ocmanager.repository.ListRepositoryStorageNodesRequest.page_request:type_name -> trpc.ocmanager.common.PageRequest + 25, // 41: trpc.ocmanager.repository.RepositoryStorageNodeResponse.storage_node:type_name -> trpc.ocmanager.repository.RepositoryStorageNode + 26, // 42: trpc.ocmanager.repository.RepositoryStorageNodeResponse.repositories:type_name -> trpc.ocmanager.repository.RepositoryNodeRepositorySummary + 34, // 43: trpc.ocmanager.repository.RepositoryStorageNodeResponse.metadata_signing_keys:type_name -> trpc.ocmanager.repository.RepositoryMetadataSigningKey + 174, // 44: trpc.ocmanager.repository.RepositoryMetadataSigningKey.key_created_at:type_name -> google.protobuf.Timestamp + 174, // 45: trpc.ocmanager.repository.RepositoryMetadataSigningKey.observed_at:type_name -> google.protobuf.Timestamp + 25, // 46: trpc.ocmanager.repository.ListRepositoryStorageNodesResponse.storage_nodes:type_name -> trpc.ocmanager.repository.RepositoryStorageNode + 176, // 47: trpc.ocmanager.repository.ListRepositoryStorageNodesResponse.page_response:type_name -> trpc.ocmanager.common.PageResponse + 172, // 48: trpc.ocmanager.repository.RepositoryNodeEnrollmentSpec.labels:type_name -> trpc.ocmanager.repository.RepositoryNodeEnrollmentSpec.LabelsEntry + 1, // 49: trpc.ocmanager.repository.RepositoryNodeEnrollmentSpec.expected_architecture:type_name -> trpc.ocmanager.repository.RepositoryArchitecture + 10, // 50: trpc.ocmanager.repository.RepositoryNodeEnrollment.status:type_name -> trpc.ocmanager.repository.RepositoryNodeEnrollmentStatus + 1, // 51: trpc.ocmanager.repository.RepositoryNodeEnrollment.expected_architecture:type_name -> trpc.ocmanager.repository.RepositoryArchitecture + 174, // 52: trpc.ocmanager.repository.RepositoryNodeEnrollment.expires_at:type_name -> google.protobuf.Timestamp + 174, // 53: trpc.ocmanager.repository.RepositoryNodeEnrollment.created_at:type_name -> google.protobuf.Timestamp + 174, // 54: trpc.ocmanager.repository.RepositoryNodeEnrollment.updated_at:type_name -> google.protobuf.Timestamp + 36, // 55: trpc.ocmanager.repository.CreateRepositoryNodeEnrollmentRequest.spec:type_name -> trpc.ocmanager.repository.RepositoryNodeEnrollmentSpec + 25, // 56: trpc.ocmanager.repository.RepositoryNodeEnrollmentResponse.storage_node:type_name -> trpc.ocmanager.repository.RepositoryStorageNode + 37, // 57: trpc.ocmanager.repository.RepositoryNodeEnrollmentResponse.enrollment:type_name -> trpc.ocmanager.repository.RepositoryNodeEnrollment + 70, // 58: trpc.ocmanager.repository.RepositoryNodeEnrollmentResponse.task:type_name -> trpc.ocmanager.repository.RepositoryTask + 72, // 59: trpc.ocmanager.repository.RepositoryNodeEnrollmentResponse.target:type_name -> trpc.ocmanager.repository.RepositoryTaskTarget + 174, // 60: trpc.ocmanager.repository.RepositoryNodeEnrollmentResponse.bootstrap_expires_at:type_name -> google.protobuf.Timestamp + 1, // 61: trpc.ocmanager.repository.RepositoryNodeArtifactSpec.architecture:type_name -> trpc.ocmanager.repository.RepositoryArchitecture + 1, // 62: trpc.ocmanager.repository.RepositoryNodeArtifact.architecture:type_name -> trpc.ocmanager.repository.RepositoryArchitecture + 174, // 63: trpc.ocmanager.repository.RepositoryNodeArtifact.created_at:type_name -> google.protobuf.Timestamp + 40, // 64: trpc.ocmanager.repository.RegisterRepositoryNodeArtifactRequest.spec:type_name -> trpc.ocmanager.repository.RepositoryNodeArtifactSpec + 41, // 65: trpc.ocmanager.repository.RepositoryNodeArtifactResponse.artifact:type_name -> trpc.ocmanager.repository.RepositoryNodeArtifact + 41, // 66: trpc.ocmanager.repository.ListRepositoryNodeArtifactsResponse.artifacts:type_name -> trpc.ocmanager.repository.RepositoryNodeArtifact + 1, // 67: trpc.ocmanager.repository.RepositoryNodeInstallSourceSpec.architecture:type_name -> trpc.ocmanager.repository.RepositoryArchitecture + 1, // 68: trpc.ocmanager.repository.RepositoryNodeInstallSource.architecture:type_name -> trpc.ocmanager.repository.RepositoryArchitecture + 174, // 69: trpc.ocmanager.repository.RepositoryNodeInstallSource.created_at:type_name -> google.protobuf.Timestamp + 174, // 70: trpc.ocmanager.repository.RepositoryNodeInstallSource.updated_at:type_name -> google.protobuf.Timestamp + 47, // 71: trpc.ocmanager.repository.ConfigureRepositoryNodeInstallSourceRequest.spec:type_name -> trpc.ocmanager.repository.RepositoryNodeInstallSourceSpec + 48, // 72: trpc.ocmanager.repository.RepositoryNodeInstallSourceResponse.install_source:type_name -> trpc.ocmanager.repository.RepositoryNodeInstallSource + 48, // 73: trpc.ocmanager.repository.ListRepositoryNodeInstallSourcesResponse.install_sources:type_name -> trpc.ocmanager.repository.RepositoryNodeInstallSource + 1, // 74: trpc.ocmanager.repository.RepositoryNodeRegistrationAllowlistSpec.expected_architecture:type_name -> trpc.ocmanager.repository.RepositoryArchitecture + 174, // 75: trpc.ocmanager.repository.RepositoryNodeRegistrationAllowlistSpec.expires_at:type_name -> google.protobuf.Timestamp + 1, // 76: trpc.ocmanager.repository.RepositoryNodeRegistrationAllowlist.expected_architecture:type_name -> trpc.ocmanager.repository.RepositoryArchitecture + 174, // 77: trpc.ocmanager.repository.RepositoryNodeRegistrationAllowlist.expires_at:type_name -> google.protobuf.Timestamp + 174, // 78: trpc.ocmanager.repository.RepositoryNodeRegistrationAllowlist.created_at:type_name -> google.protobuf.Timestamp + 174, // 79: trpc.ocmanager.repository.RepositoryNodeRegistrationAllowlist.updated_at:type_name -> google.protobuf.Timestamp + 53, // 80: trpc.ocmanager.repository.UpsertRepositoryNodeRegistrationAllowlistRequest.spec:type_name -> trpc.ocmanager.repository.RepositoryNodeRegistrationAllowlistSpec + 54, // 81: trpc.ocmanager.repository.RepositoryNodeRegistrationAllowlistResponse.allowlist:type_name -> trpc.ocmanager.repository.RepositoryNodeRegistrationAllowlist + 54, // 82: trpc.ocmanager.repository.ListRepositoryNodeRegistrationAllowlistsResponse.allowlists:type_name -> trpc.ocmanager.repository.RepositoryNodeRegistrationAllowlist + 173, // 83: trpc.ocmanager.repository.RepositoryPreflightRecord.tool_versions:type_name -> trpc.ocmanager.repository.RepositoryPreflightRecord.ToolVersionsEntry + 174, // 84: trpc.ocmanager.repository.RepositoryPreflightRecord.expires_at:type_name -> google.protobuf.Timestamp + 174, // 85: trpc.ocmanager.repository.RepositoryPreflightRecord.created_at:type_name -> google.protobuf.Timestamp + 25, // 86: trpc.ocmanager.repository.RepositoryStorageNodeProbeResponse.storage_node:type_name -> trpc.ocmanager.repository.RepositoryStorageNode + 59, // 87: trpc.ocmanager.repository.RepositoryStorageNodeProbeResponse.preflight:type_name -> trpc.ocmanager.repository.RepositoryPreflightRecord + 175, // 88: trpc.ocmanager.repository.ListRepositoryPreflightRecordsRequest.page_request:type_name -> trpc.ocmanager.common.PageRequest + 59, // 89: trpc.ocmanager.repository.ListRepositoryPreflightRecordsResponse.records:type_name -> trpc.ocmanager.repository.RepositoryPreflightRecord + 176, // 90: trpc.ocmanager.repository.ListRepositoryPreflightRecordsResponse.page_response:type_name -> trpc.ocmanager.common.PageResponse + 5, // 91: trpc.ocmanager.repository.RepositorySyncScheduleSpec.cadence:type_name -> trpc.ocmanager.repository.RepositorySyncScheduleCadence + 5, // 92: trpc.ocmanager.repository.RepositorySyncSchedule.cadence:type_name -> trpc.ocmanager.repository.RepositorySyncScheduleCadence + 6, // 93: trpc.ocmanager.repository.RepositorySyncSchedule.status:type_name -> trpc.ocmanager.repository.RepositorySyncScheduleStatus + 174, // 94: trpc.ocmanager.repository.RepositorySyncSchedule.last_run_at:type_name -> google.protobuf.Timestamp + 174, // 95: trpc.ocmanager.repository.RepositorySyncSchedule.next_run_at:type_name -> google.protobuf.Timestamp + 174, // 96: trpc.ocmanager.repository.RepositorySyncSchedule.created_at:type_name -> google.protobuf.Timestamp + 174, // 97: trpc.ocmanager.repository.RepositorySyncSchedule.updated_at:type_name -> google.protobuf.Timestamp + 64, // 98: trpc.ocmanager.repository.UpsertRepositorySyncScheduleRequest.spec:type_name -> trpc.ocmanager.repository.RepositorySyncScheduleSpec + 65, // 99: trpc.ocmanager.repository.RepositorySyncScheduleResponse.schedule:type_name -> trpc.ocmanager.repository.RepositorySyncSchedule + 7, // 100: trpc.ocmanager.repository.RepositoryTask.type:type_name -> trpc.ocmanager.repository.RepositoryTaskType + 8, // 101: trpc.ocmanager.repository.RepositoryTask.status:type_name -> trpc.ocmanager.repository.RepositoryTaskStatus + 174, // 102: trpc.ocmanager.repository.RepositoryTask.lease_expires_at:type_name -> google.protobuf.Timestamp + 174, // 103: trpc.ocmanager.repository.RepositoryTask.last_heartbeat_at:type_name -> google.protobuf.Timestamp + 174, // 104: trpc.ocmanager.repository.RepositoryTask.available_at:type_name -> google.protobuf.Timestamp + 174, // 105: trpc.ocmanager.repository.RepositoryTask.started_at:type_name -> google.protobuf.Timestamp + 174, // 106: trpc.ocmanager.repository.RepositoryTask.finished_at:type_name -> google.protobuf.Timestamp + 174, // 107: trpc.ocmanager.repository.RepositoryTask.cancel_requested_at:type_name -> google.protobuf.Timestamp + 174, // 108: trpc.ocmanager.repository.RepositoryTask.created_at:type_name -> google.protobuf.Timestamp + 174, // 109: trpc.ocmanager.repository.RepositoryTask.updated_at:type_name -> google.protobuf.Timestamp + 116, // 110: trpc.ocmanager.repository.RepositoryTask.diagnosis:type_name -> trpc.ocmanager.repository.RepositoryDiagnosis + 174, // 111: trpc.ocmanager.repository.RepositoryTask.prepared_at:type_name -> google.protobuf.Timestamp + 174, // 112: trpc.ocmanager.repository.RepositoryTask.activated_at:type_name -> google.protobuf.Timestamp + 174, // 113: trpc.ocmanager.repository.RepositoryTask.confirmed_at:type_name -> google.protobuf.Timestamp + 72, // 114: trpc.ocmanager.repository.RepositoryTask.targets:type_name -> trpc.ocmanager.repository.RepositoryTaskTarget + 71, // 115: trpc.ocmanager.repository.RepositoryTask.result_items:type_name -> trpc.ocmanager.repository.RepositoryTaskResultItem + 8, // 116: trpc.ocmanager.repository.RepositoryTaskResultItem.status:type_name -> trpc.ocmanager.repository.RepositoryTaskStatus + 174, // 117: trpc.ocmanager.repository.RepositoryTaskResultItem.started_at:type_name -> google.protobuf.Timestamp + 174, // 118: trpc.ocmanager.repository.RepositoryTaskResultItem.finished_at:type_name -> google.protobuf.Timestamp + 174, // 119: trpc.ocmanager.repository.RepositoryTaskResultItem.created_at:type_name -> google.protobuf.Timestamp + 8, // 120: trpc.ocmanager.repository.RepositoryTaskTarget.status:type_name -> trpc.ocmanager.repository.RepositoryTaskStatus + 174, // 121: trpc.ocmanager.repository.RepositoryTaskTarget.started_at:type_name -> google.protobuf.Timestamp + 174, // 122: trpc.ocmanager.repository.RepositoryTaskTarget.finished_at:type_name -> google.protobuf.Timestamp + 174, // 123: trpc.ocmanager.repository.RepositoryTaskTarget.created_at:type_name -> google.protobuf.Timestamp + 174, // 124: trpc.ocmanager.repository.RepositoryTaskTarget.updated_at:type_name -> google.protobuf.Timestamp + 8, // 125: trpc.ocmanager.repository.ListRepositoryTasksRequest.status:type_name -> trpc.ocmanager.repository.RepositoryTaskStatus + 7, // 126: trpc.ocmanager.repository.ListRepositoryTasksRequest.type:type_name -> trpc.ocmanager.repository.RepositoryTaskType + 174, // 127: trpc.ocmanager.repository.ListRepositoryTasksRequest.created_at_from:type_name -> google.protobuf.Timestamp + 174, // 128: trpc.ocmanager.repository.ListRepositoryTasksRequest.created_at_to:type_name -> google.protobuf.Timestamp + 175, // 129: trpc.ocmanager.repository.ListRepositoryTasksRequest.page_request:type_name -> trpc.ocmanager.common.PageRequest + 70, // 130: trpc.ocmanager.repository.RepositoryTaskResponse.task:type_name -> trpc.ocmanager.repository.RepositoryTask + 116, // 131: trpc.ocmanager.repository.RepositoryOCAIContext.diagnosis:type_name -> trpc.ocmanager.repository.RepositoryDiagnosis + 174, // 132: trpc.ocmanager.repository.RepositoryOCAIAnalysis.created_at:type_name -> google.protobuf.Timestamp + 174, // 133: trpc.ocmanager.repository.RepositoryOCAIAnalysis.feedback_at:type_name -> google.protobuf.Timestamp + 79, // 134: trpc.ocmanager.repository.RepositoryOCAIContextResponse.context:type_name -> trpc.ocmanager.repository.RepositoryOCAIContext + 80, // 135: trpc.ocmanager.repository.RepositoryOCAIAnalysisResponse.analysis:type_name -> trpc.ocmanager.repository.RepositoryOCAIAnalysis + 79, // 136: trpc.ocmanager.repository.RepositoryOCAIAnalysisResponse.context:type_name -> trpc.ocmanager.repository.RepositoryOCAIContext + 88, // 137: trpc.ocmanager.repository.RepositoryConfigResponse.config:type_name -> trpc.ocmanager.repository.RepositoryConfig + 174, // 138: trpc.ocmanager.repository.RepositoryGPGKey.created_at:type_name -> google.protobuf.Timestamp + 90, // 139: trpc.ocmanager.repository.RepositoryGPGKeyResponse.key:type_name -> trpc.ocmanager.repository.RepositoryGPGKey + 90, // 140: trpc.ocmanager.repository.ListRepositoryGPGKeysResponse.keys:type_name -> trpc.ocmanager.repository.RepositoryGPGKey + 70, // 141: trpc.ocmanager.repository.ListRepositoryTasksResponse.tasks:type_name -> trpc.ocmanager.repository.RepositoryTask + 75, // 142: trpc.ocmanager.repository.ListRepositoryTasksResponse.statistics:type_name -> trpc.ocmanager.repository.RepositoryTaskStatistics + 176, // 143: trpc.ocmanager.repository.ListRepositoryTasksResponse.page_response:type_name -> trpc.ocmanager.common.PageResponse + 174, // 144: trpc.ocmanager.repository.RepositoryUpload.expires_at:type_name -> google.protobuf.Timestamp + 174, // 145: trpc.ocmanager.repository.RepositoryUpload.completed_at:type_name -> google.protobuf.Timestamp + 174, // 146: trpc.ocmanager.repository.RepositoryUpload.created_at:type_name -> google.protobuf.Timestamp + 174, // 147: trpc.ocmanager.repository.RepositoryUpload.updated_at:type_name -> google.protobuf.Timestamp + 174, // 148: trpc.ocmanager.repository.RepositoryUpload.paused_at:type_name -> google.protobuf.Timestamp + 174, // 149: trpc.ocmanager.repository.RepositoryUploadChunk.created_at:type_name -> google.protobuf.Timestamp + 97, // 150: trpc.ocmanager.repository.PutRepositoryUploadChunkResponse.upload:type_name -> trpc.ocmanager.repository.RepositoryUpload + 101, // 151: trpc.ocmanager.repository.PutRepositoryUploadChunkResponse.chunk:type_name -> trpc.ocmanager.repository.RepositoryUploadChunk + 97, // 152: trpc.ocmanager.repository.DescribeRepositoryUploadChunksResponse.upload:type_name -> trpc.ocmanager.repository.RepositoryUpload + 101, // 153: trpc.ocmanager.repository.DescribeRepositoryUploadChunksResponse.chunks:type_name -> trpc.ocmanager.repository.RepositoryUploadChunk + 97, // 154: trpc.ocmanager.repository.RepositoryUploadResponse.upload:type_name -> trpc.ocmanager.repository.RepositoryUpload + 97, // 155: trpc.ocmanager.repository.CompleteRepositoryUploadResponse.upload:type_name -> trpc.ocmanager.repository.RepositoryUpload + 70, // 156: trpc.ocmanager.repository.CompleteRepositoryUploadResponse.task:type_name -> trpc.ocmanager.repository.RepositoryTask + 174, // 157: trpc.ocmanager.repository.RepositoryPackage.created_at:type_name -> google.protobuf.Timestamp + 1, // 158: trpc.ocmanager.repository.ListRepositoryPackagesRequest.architecture:type_name -> trpc.ocmanager.repository.RepositoryArchitecture + 175, // 159: trpc.ocmanager.repository.ListRepositoryPackagesRequest.page_request:type_name -> trpc.ocmanager.common.PageRequest + 109, // 160: trpc.ocmanager.repository.ListRepositoryPackagesResponse.packages:type_name -> trpc.ocmanager.repository.RepositoryPackage + 176, // 161: trpc.ocmanager.repository.ListRepositoryPackagesResponse.page_response:type_name -> trpc.ocmanager.common.PageResponse + 174, // 162: trpc.ocmanager.repository.RepositoryQuarantineItem.created_at:type_name -> google.protobuf.Timestamp + 175, // 163: trpc.ocmanager.repository.ListRepositoryQuarantineRequest.page_request:type_name -> trpc.ocmanager.common.PageRequest + 112, // 164: trpc.ocmanager.repository.ListRepositoryQuarantineResponse.items:type_name -> trpc.ocmanager.repository.RepositoryQuarantineItem + 176, // 165: trpc.ocmanager.repository.ListRepositoryQuarantineResponse.page_response:type_name -> trpc.ocmanager.common.PageResponse + 174, // 166: trpc.ocmanager.repository.RepositoryDiagnosis.generated_at:type_name -> google.protobuf.Timestamp + 174, // 167: trpc.ocmanager.repository.RepositoryDistributionNode.last_heartbeat_at:type_name -> google.protobuf.Timestamp + 119, // 168: trpc.ocmanager.repository.ListRepositoryDistributionNodesResponse.nodes:type_name -> trpc.ocmanager.repository.RepositoryDistributionNode + 175, // 169: trpc.ocmanager.repository.ListRepositoryDistributionsRequest.page_request:type_name -> trpc.ocmanager.common.PageRequest + 174, // 170: trpc.ocmanager.repository.RepositoryDistributionTarget.started_at:type_name -> google.protobuf.Timestamp + 174, // 171: trpc.ocmanager.repository.RepositoryDistributionTarget.finished_at:type_name -> google.protobuf.Timestamp + 125, // 172: trpc.ocmanager.repository.RepositoryDistribution.targets:type_name -> trpc.ocmanager.repository.RepositoryDistributionTarget + 174, // 173: trpc.ocmanager.repository.RepositoryDistribution.created_at:type_name -> google.protobuf.Timestamp + 174, // 174: trpc.ocmanager.repository.RepositoryDistribution.started_at:type_name -> google.protobuf.Timestamp + 174, // 175: trpc.ocmanager.repository.RepositoryDistribution.finished_at:type_name -> google.protobuf.Timestamp + 174, // 176: trpc.ocmanager.repository.RepositoryDistribution.updated_at:type_name -> google.protobuf.Timestamp + 126, // 177: trpc.ocmanager.repository.RepositoryDistributionResponse.distribution:type_name -> trpc.ocmanager.repository.RepositoryDistribution + 70, // 178: trpc.ocmanager.repository.RepositoryDistributionResponse.task:type_name -> trpc.ocmanager.repository.RepositoryTask + 126, // 179: trpc.ocmanager.repository.ListRepositoryDistributionsResponse.distributions:type_name -> trpc.ocmanager.repository.RepositoryDistribution + 176, // 180: trpc.ocmanager.repository.ListRepositoryDistributionsResponse.page_response:type_name -> trpc.ocmanager.common.PageResponse + 131, // 181: trpc.ocmanager.repository.RepositoryPackageDeletionPreview.repository_set:type_name -> trpc.ocmanager.repository.RepositoryPackageDeletionPreviewSource + 132, // 182: trpc.ocmanager.repository.RepositoryPackageDeletionPreview.dependency_impact:type_name -> trpc.ocmanager.repository.RepositoryPackageDependencyImpact + 174, // 183: trpc.ocmanager.repository.RepositoryPackageDeletionPreview.expires_at:type_name -> google.protobuf.Timestamp + 133, // 184: trpc.ocmanager.repository.RepositoryPackageDeletionPreviewResponse.preview:type_name -> trpc.ocmanager.repository.RepositoryPackageDeletionPreview + 8, // 185: trpc.ocmanager.repository.RepositoryPackageInstallPreviewTarget.status:type_name -> trpc.ocmanager.repository.RepositoryTaskStatus + 174, // 186: trpc.ocmanager.repository.RepositoryPackageInstallPreviewTarget.started_at:type_name -> google.protobuf.Timestamp + 174, // 187: trpc.ocmanager.repository.RepositoryPackageInstallPreviewTarget.finished_at:type_name -> google.protobuf.Timestamp + 136, // 188: trpc.ocmanager.repository.RepositoryPackageInstallPreview.targets:type_name -> trpc.ocmanager.repository.RepositoryPackageInstallPreviewTarget + 174, // 189: trpc.ocmanager.repository.RepositoryPackageInstallPreview.expires_at:type_name -> google.protobuf.Timestamp + 137, // 190: trpc.ocmanager.repository.RepositoryPackageInstallPreviewResponse.preview:type_name -> trpc.ocmanager.repository.RepositoryPackageInstallPreview + 174, // 191: trpc.ocmanager.repository.RepositoryPackageInstall.created_at:type_name -> google.protobuf.Timestamp + 174, // 192: trpc.ocmanager.repository.RepositoryPackageInstall.updated_at:type_name -> google.protobuf.Timestamp + 141, // 193: trpc.ocmanager.repository.RepositoryPackageInstallResponse.install:type_name -> trpc.ocmanager.repository.RepositoryPackageInstall + 70, // 194: trpc.ocmanager.repository.RepositoryPackageInstallResponse.task:type_name -> trpc.ocmanager.repository.RepositoryTask + 4, // 195: trpc.ocmanager.repository.PreviewRepositoryInventoryImportRequest.conflict_strategy:type_name -> trpc.ocmanager.repository.RepositoryInventoryConflictStrategy + 147, // 196: trpc.ocmanager.repository.PreviewRepositoryInventoryImportRequest.storage_node_mappings:type_name -> trpc.ocmanager.repository.RepositoryInventoryStorageNodeMapping + 148, // 197: trpc.ocmanager.repository.PreviewRepositoryInventoryImportRequest.signing_key_mappings:type_name -> trpc.ocmanager.repository.RepositoryInventorySigningKeyMapping + 11, // 198: trpc.ocmanager.repository.RepositoryInventoryImportItem.spec:type_name -> trpc.ocmanager.repository.RepositorySpec + 174, // 199: trpc.ocmanager.repository.RepositoryInventoryImportPreviewResponse.expires_at:type_name -> google.protobuf.Timestamp + 150, // 200: trpc.ocmanager.repository.RepositoryInventoryImportPreviewResponse.items:type_name -> trpc.ocmanager.repository.RepositoryInventoryImportItem + 174, // 201: trpc.ocmanager.repository.RepositoryInventoryImportJob.created_at:type_name -> google.protobuf.Timestamp + 174, // 202: trpc.ocmanager.repository.RepositoryInventoryImportJob.finished_at:type_name -> google.protobuf.Timestamp + 153, // 203: trpc.ocmanager.repository.RepositoryInventoryImportResponse.job:type_name -> trpc.ocmanager.repository.RepositoryInventoryImportJob + 12, // 204: trpc.ocmanager.repository.RepositoryInventoryImportResponse.repositories:type_name -> trpc.ocmanager.repository.Repository + 174, // 205: trpc.ocmanager.repository.RepositoryTaskLog.expires_at:type_name -> google.protobuf.Timestamp + 174, // 206: trpc.ocmanager.repository.RepositoryTaskLog.created_at:type_name -> google.protobuf.Timestamp + 158, // 207: trpc.ocmanager.repository.RepositoryTaskLogResponse.task_log:type_name -> trpc.ocmanager.repository.RepositoryTaskLog + 1, // 208: trpc.ocmanager.repository.RepositoryUpstreamProfileSpec.supported_architectures:type_name -> trpc.ocmanager.repository.RepositoryArchitecture + 1, // 209: trpc.ocmanager.repository.RepositoryUpstreamProfile.supported_architectures:type_name -> trpc.ocmanager.repository.RepositoryArchitecture + 174, // 210: trpc.ocmanager.repository.RepositoryUpstreamProfile.created_at:type_name -> google.protobuf.Timestamp + 174, // 211: trpc.ocmanager.repository.RepositoryUpstreamProfile.updated_at:type_name -> google.protobuf.Timestamp + 160, // 212: trpc.ocmanager.repository.CreateRepositoryUpstreamProfileRequest.spec:type_name -> trpc.ocmanager.repository.RepositoryUpstreamProfileSpec + 160, // 213: trpc.ocmanager.repository.UpdateRepositoryUpstreamProfileRequest.spec:type_name -> trpc.ocmanager.repository.RepositoryUpstreamProfileSpec + 175, // 214: trpc.ocmanager.repository.ListRepositoryUpstreamProfilesRequest.page_request:type_name -> trpc.ocmanager.common.PageRequest + 161, // 215: trpc.ocmanager.repository.RepositoryUpstreamProfileResponse.profile:type_name -> trpc.ocmanager.repository.RepositoryUpstreamProfile + 161, // 216: trpc.ocmanager.repository.ListRepositoryUpstreamProfilesResponse.profiles:type_name -> trpc.ocmanager.repository.RepositoryUpstreamProfile + 176, // 217: trpc.ocmanager.repository.ListRepositoryUpstreamProfilesResponse.page_response:type_name -> trpc.ocmanager.common.PageResponse + 13, // 218: trpc.ocmanager.repository.RepositoryService.CreateRepository:input_type -> trpc.ocmanager.repository.CreateRepositoryRequest + 14, // 219: trpc.ocmanager.repository.RepositoryService.UpdateRepository:input_type -> trpc.ocmanager.repository.UpdateRepositoryRequest + 15, // 220: trpc.ocmanager.repository.RepositoryService.DescribeRepository:input_type -> trpc.ocmanager.repository.DescribeRepositoryRequest + 16, // 221: trpc.ocmanager.repository.RepositoryService.ListRepositories:input_type -> trpc.ocmanager.repository.ListRepositoriesRequest + 17, // 222: trpc.ocmanager.repository.RepositoryService.DeleteRepository:input_type -> trpc.ocmanager.repository.DeleteRepositoryRequest + 19, // 223: trpc.ocmanager.repository.RepositoryService.BatchApplyRepositories:input_type -> trpc.ocmanager.repository.BatchApplyRepositoriesRequest + 162, // 224: trpc.ocmanager.repository.RepositoryService.CreateRepositoryUpstreamProfile:input_type -> trpc.ocmanager.repository.CreateRepositoryUpstreamProfileRequest + 163, // 225: trpc.ocmanager.repository.RepositoryService.UpdateRepositoryUpstreamProfile:input_type -> trpc.ocmanager.repository.UpdateRepositoryUpstreamProfileRequest + 164, // 226: trpc.ocmanager.repository.RepositoryService.DescribeRepositoryUpstreamProfile:input_type -> trpc.ocmanager.repository.DescribeRepositoryUpstreamProfileRequest + 165, // 227: trpc.ocmanager.repository.RepositoryService.ListRepositoryUpstreamProfiles:input_type -> trpc.ocmanager.repository.ListRepositoryUpstreamProfilesRequest + 166, // 228: trpc.ocmanager.repository.RepositoryService.DeleteRepositoryUpstreamProfile:input_type -> trpc.ocmanager.repository.DeleteRepositoryUpstreamProfileRequest + 27, // 229: trpc.ocmanager.repository.RepositoryService.CreateRepositoryStorageNode:input_type -> trpc.ocmanager.repository.CreateRepositoryStorageNodeRequest + 28, // 230: trpc.ocmanager.repository.RepositoryService.UpdateRepositoryStorageNode:input_type -> trpc.ocmanager.repository.UpdateRepositoryStorageNodeRequest + 29, // 231: trpc.ocmanager.repository.RepositoryService.DescribeRepositoryStorageNode:input_type -> trpc.ocmanager.repository.DescribeRepositoryStorageNodeRequest + 30, // 232: trpc.ocmanager.repository.RepositoryService.ListRepositoryStorageNodes:input_type -> trpc.ocmanager.repository.ListRepositoryStorageNodesRequest + 31, // 233: trpc.ocmanager.repository.RepositoryService.DeleteRepositoryStorageNode:input_type -> trpc.ocmanager.repository.DeleteRepositoryStorageNodeRequest + 32, // 234: trpc.ocmanager.repository.RepositoryService.ProbeRepositoryStorageNode:input_type -> trpc.ocmanager.repository.ProbeRepositoryStorageNodeRequest + 61, // 235: trpc.ocmanager.repository.RepositoryService.ListRepositoryPreflightRecords:input_type -> trpc.ocmanager.repository.ListRepositoryPreflightRecordsRequest + 38, // 236: trpc.ocmanager.repository.RepositoryService.CreateRepositoryNodeEnrollment:input_type -> trpc.ocmanager.repository.CreateRepositoryNodeEnrollmentRequest + 42, // 237: trpc.ocmanager.repository.RepositoryService.RegisterRepositoryNodeArtifact:input_type -> trpc.ocmanager.repository.RegisterRepositoryNodeArtifactRequest + 44, // 238: trpc.ocmanager.repository.RepositoryService.ListRepositoryNodeArtifacts:input_type -> trpc.ocmanager.repository.ListRepositoryNodeArtifactsRequest + 46, // 239: trpc.ocmanager.repository.RepositoryService.DownloadRepositoryNodeArtifact:input_type -> trpc.ocmanager.repository.DownloadRepositoryNodeArtifactRequest + 49, // 240: trpc.ocmanager.repository.RepositoryService.ConfigureRepositoryNodeInstallSource:input_type -> trpc.ocmanager.repository.ConfigureRepositoryNodeInstallSourceRequest + 51, // 241: trpc.ocmanager.repository.RepositoryService.ListRepositoryNodeInstallSources:input_type -> trpc.ocmanager.repository.ListRepositoryNodeInstallSourcesRequest + 55, // 242: trpc.ocmanager.repository.RepositoryService.UpsertRepositoryNodeRegistrationAllowlist:input_type -> trpc.ocmanager.repository.UpsertRepositoryNodeRegistrationAllowlistRequest + 57, // 243: trpc.ocmanager.repository.RepositoryService.ListRepositoryNodeRegistrationAllowlists:input_type -> trpc.ocmanager.repository.ListRepositoryNodeRegistrationAllowlistsRequest + 63, // 244: trpc.ocmanager.repository.RepositoryService.SyncRepository:input_type -> trpc.ocmanager.repository.SyncRepositoryRequest + 66, // 245: trpc.ocmanager.repository.RepositoryService.UpsertRepositorySyncSchedule:input_type -> trpc.ocmanager.repository.UpsertRepositorySyncScheduleRequest + 67, // 246: trpc.ocmanager.repository.RepositoryService.DescribeRepositorySyncSchedule:input_type -> trpc.ocmanager.repository.DescribeRepositorySyncScheduleRequest + 68, // 247: trpc.ocmanager.repository.RepositoryService.PauseRepositorySyncSchedule:input_type -> trpc.ocmanager.repository.ChangeRepositorySyncScheduleStatusRequest + 68, // 248: trpc.ocmanager.repository.RepositoryService.ResumeRepositorySyncSchedule:input_type -> trpc.ocmanager.repository.ChangeRepositorySyncScheduleStatusRequest + 87, // 249: trpc.ocmanager.repository.RepositoryService.GenerateRepositoryConfig:input_type -> trpc.ocmanager.repository.GenerateRepositoryConfigRequest + 91, // 250: trpc.ocmanager.repository.RepositoryService.RegisterRepositoryGPGKey:input_type -> trpc.ocmanager.repository.RegisterRepositoryGPGKeyRequest + 92, // 251: trpc.ocmanager.repository.RepositoryService.ListRepositoryGPGKeys:input_type -> trpc.ocmanager.repository.ListRepositoryGPGKeysRequest + 93, // 252: trpc.ocmanager.repository.RepositoryService.DeleteRepositoryGPGKey:input_type -> trpc.ocmanager.repository.DeleteRepositoryGPGKeyRequest + 73, // 253: trpc.ocmanager.repository.RepositoryService.DescribeRepositoryTask:input_type -> trpc.ocmanager.repository.DescribeRepositoryTaskRequest + 74, // 254: trpc.ocmanager.repository.RepositoryService.ListRepositoryTasks:input_type -> trpc.ocmanager.repository.ListRepositoryTasksRequest + 76, // 255: trpc.ocmanager.repository.RepositoryService.CancelRepositoryTask:input_type -> trpc.ocmanager.repository.CancelRepositoryTaskRequest + 77, // 256: trpc.ocmanager.repository.RepositoryService.RetryRepositoryTask:input_type -> trpc.ocmanager.repository.RetryRepositoryTaskRequest + 117, // 257: trpc.ocmanager.repository.RepositoryService.ExportRepositoryTaskDiagnosis:input_type -> trpc.ocmanager.repository.ExportRepositoryTaskDiagnosisRequest + 156, // 258: trpc.ocmanager.repository.RepositoryService.DescribeRepositoryTaskLog:input_type -> trpc.ocmanager.repository.DescribeRepositoryTaskLogRequest + 157, // 259: trpc.ocmanager.repository.RepositoryService.DownloadRepositoryTaskLog:input_type -> trpc.ocmanager.repository.DownloadRepositoryTaskLogRequest + 81, // 260: trpc.ocmanager.repository.RepositoryService.BuildRepositoryOCAIContext:input_type -> trpc.ocmanager.repository.BuildRepositoryOCAIContextRequest + 82, // 261: trpc.ocmanager.repository.RepositoryService.CreateRepositoryOCAIAnalysis:input_type -> trpc.ocmanager.repository.CreateRepositoryOCAIAnalysisRequest + 83, // 262: trpc.ocmanager.repository.RepositoryService.DescribeRepositoryOCAIAnalysis:input_type -> trpc.ocmanager.repository.DescribeRepositoryOCAIAnalysisRequest + 84, // 263: trpc.ocmanager.repository.RepositoryService.AppendRepositoryOCAIFeedback:input_type -> trpc.ocmanager.repository.AppendRepositoryOCAIFeedbackRequest + 98, // 264: trpc.ocmanager.repository.RepositoryService.BeginRepositoryUpload:input_type -> trpc.ocmanager.repository.BeginRepositoryUploadRequest + 99, // 265: trpc.ocmanager.repository.RepositoryService.AppendRepositoryUploadChunk:input_type -> trpc.ocmanager.repository.AppendRepositoryUploadChunkRequest + 100, // 266: trpc.ocmanager.repository.RepositoryService.PutRepositoryUploadChunk:input_type -> trpc.ocmanager.repository.PutRepositoryUploadChunkRequest + 106, // 267: trpc.ocmanager.repository.RepositoryService.DescribeRepositoryUploadChunks:input_type -> trpc.ocmanager.repository.DescribeRepositoryUploadRequest + 106, // 268: trpc.ocmanager.repository.RepositoryService.PauseRepositoryUpload:input_type -> trpc.ocmanager.repository.DescribeRepositoryUploadRequest + 106, // 269: trpc.ocmanager.repository.RepositoryService.ResumeRepositoryUpload:input_type -> trpc.ocmanager.repository.DescribeRepositoryUploadRequest + 104, // 270: trpc.ocmanager.repository.RepositoryService.CompleteRepositoryUpload:input_type -> trpc.ocmanager.repository.CompleteRepositoryUploadRequest + 105, // 271: trpc.ocmanager.repository.RepositoryService.AbortRepositoryUpload:input_type -> trpc.ocmanager.repository.AbortRepositoryUploadRequest + 106, // 272: trpc.ocmanager.repository.RepositoryService.DescribeRepositoryUpload:input_type -> trpc.ocmanager.repository.DescribeRepositoryUploadRequest + 110, // 273: trpc.ocmanager.repository.RepositoryService.ListRepositoryPackages:input_type -> trpc.ocmanager.repository.ListRepositoryPackagesRequest + 113, // 274: trpc.ocmanager.repository.RepositoryService.ListRepositoryQuarantine:input_type -> trpc.ocmanager.repository.ListRepositoryQuarantineRequest + 115, // 275: trpc.ocmanager.repository.RepositoryService.DeleteRepositoryPackage:input_type -> trpc.ocmanager.repository.DeleteRepositoryPackageRequest + 129, // 276: trpc.ocmanager.repository.RepositoryService.PreviewRepositoryPackageDeletion:input_type -> trpc.ocmanager.repository.PreviewRepositoryPackageDeletionRequest + 130, // 277: trpc.ocmanager.repository.RepositoryService.ConfirmRepositoryPackageDeletion:input_type -> trpc.ocmanager.repository.ConfirmRepositoryPackageDeletionRequest + 120, // 278: trpc.ocmanager.repository.RepositoryService.ListRepositoryDistributionNodes:input_type -> trpc.ocmanager.repository.ListRepositoryDistributionNodesRequest + 122, // 279: trpc.ocmanager.repository.RepositoryService.CreateRepositoryDistribution:input_type -> trpc.ocmanager.repository.CreateRepositoryDistributionRequest + 123, // 280: trpc.ocmanager.repository.RepositoryService.DescribeRepositoryDistribution:input_type -> trpc.ocmanager.repository.DescribeRepositoryDistributionRequest + 124, // 281: trpc.ocmanager.repository.RepositoryService.ListRepositoryDistributions:input_type -> trpc.ocmanager.repository.ListRepositoryDistributionsRequest + 135, // 282: trpc.ocmanager.repository.RepositoryService.PreviewRepositoryPackageInstall:input_type -> trpc.ocmanager.repository.PreviewRepositoryPackageInstallRequest + 139, // 283: trpc.ocmanager.repository.RepositoryService.ConfirmRepositoryPackageInstall:input_type -> trpc.ocmanager.repository.ConfirmRepositoryPackageInstallRequest + 140, // 284: trpc.ocmanager.repository.RepositoryService.DescribeRepositoryPackageInstall:input_type -> trpc.ocmanager.repository.DescribeRepositoryPackageInstallRequest + 143, // 285: trpc.ocmanager.repository.RepositoryService.ExportRepositoryInventory:input_type -> trpc.ocmanager.repository.ExportRepositoryInventoryRequest + 149, // 286: trpc.ocmanager.repository.RepositoryService.PreviewRepositoryInventoryImport:input_type -> trpc.ocmanager.repository.PreviewRepositoryInventoryImportRequest + 152, // 287: trpc.ocmanager.repository.RepositoryService.ConfirmRepositoryInventoryImport:input_type -> trpc.ocmanager.repository.ConfirmRepositoryInventoryImportRequest + 144, // 288: trpc.ocmanager.repository.RepositoryService.ExportRepositoryValidationReport:input_type -> trpc.ocmanager.repository.ExportRepositoryValidationReportRequest + 145, // 289: trpc.ocmanager.repository.RepositoryService.ExportRepositoryDistributionResult:input_type -> trpc.ocmanager.repository.ExportRepositoryDistributionResultRequest + 155, // 290: trpc.ocmanager.repository.RepositoryService.MigrateRepository:input_type -> trpc.ocmanager.repository.MigrateRepositoryRequest + 21, // 291: trpc.ocmanager.repository.RepositoryService.CreateRepository:output_type -> trpc.ocmanager.repository.RepositoryResponse + 21, // 292: trpc.ocmanager.repository.RepositoryService.UpdateRepository:output_type -> trpc.ocmanager.repository.RepositoryResponse + 21, // 293: trpc.ocmanager.repository.RepositoryService.DescribeRepository:output_type -> trpc.ocmanager.repository.RepositoryResponse + 22, // 294: trpc.ocmanager.repository.RepositoryService.ListRepositories:output_type -> trpc.ocmanager.repository.ListRepositoriesResponse + 177, // 295: trpc.ocmanager.repository.RepositoryService.DeleteRepository:output_type -> google.protobuf.Empty + 23, // 296: trpc.ocmanager.repository.RepositoryService.BatchApplyRepositories:output_type -> trpc.ocmanager.repository.BatchApplyRepositoriesResponse + 167, // 297: trpc.ocmanager.repository.RepositoryService.CreateRepositoryUpstreamProfile:output_type -> trpc.ocmanager.repository.RepositoryUpstreamProfileResponse + 167, // 298: trpc.ocmanager.repository.RepositoryService.UpdateRepositoryUpstreamProfile:output_type -> trpc.ocmanager.repository.RepositoryUpstreamProfileResponse + 167, // 299: trpc.ocmanager.repository.RepositoryService.DescribeRepositoryUpstreamProfile:output_type -> trpc.ocmanager.repository.RepositoryUpstreamProfileResponse + 168, // 300: trpc.ocmanager.repository.RepositoryService.ListRepositoryUpstreamProfiles:output_type -> trpc.ocmanager.repository.ListRepositoryUpstreamProfilesResponse + 177, // 301: trpc.ocmanager.repository.RepositoryService.DeleteRepositoryUpstreamProfile:output_type -> google.protobuf.Empty + 33, // 302: trpc.ocmanager.repository.RepositoryService.CreateRepositoryStorageNode:output_type -> trpc.ocmanager.repository.RepositoryStorageNodeResponse + 33, // 303: trpc.ocmanager.repository.RepositoryService.UpdateRepositoryStorageNode:output_type -> trpc.ocmanager.repository.RepositoryStorageNodeResponse + 33, // 304: trpc.ocmanager.repository.RepositoryService.DescribeRepositoryStorageNode:output_type -> trpc.ocmanager.repository.RepositoryStorageNodeResponse + 35, // 305: trpc.ocmanager.repository.RepositoryService.ListRepositoryStorageNodes:output_type -> trpc.ocmanager.repository.ListRepositoryStorageNodesResponse + 177, // 306: trpc.ocmanager.repository.RepositoryService.DeleteRepositoryStorageNode:output_type -> google.protobuf.Empty + 60, // 307: trpc.ocmanager.repository.RepositoryService.ProbeRepositoryStorageNode:output_type -> trpc.ocmanager.repository.RepositoryStorageNodeProbeResponse + 62, // 308: trpc.ocmanager.repository.RepositoryService.ListRepositoryPreflightRecords:output_type -> trpc.ocmanager.repository.ListRepositoryPreflightRecordsResponse + 39, // 309: trpc.ocmanager.repository.RepositoryService.CreateRepositoryNodeEnrollment:output_type -> trpc.ocmanager.repository.RepositoryNodeEnrollmentResponse + 43, // 310: trpc.ocmanager.repository.RepositoryService.RegisterRepositoryNodeArtifact:output_type -> trpc.ocmanager.repository.RepositoryNodeArtifactResponse + 45, // 311: trpc.ocmanager.repository.RepositoryService.ListRepositoryNodeArtifacts:output_type -> trpc.ocmanager.repository.ListRepositoryNodeArtifactsResponse + 177, // 312: trpc.ocmanager.repository.RepositoryService.DownloadRepositoryNodeArtifact:output_type -> google.protobuf.Empty + 50, // 313: trpc.ocmanager.repository.RepositoryService.ConfigureRepositoryNodeInstallSource:output_type -> trpc.ocmanager.repository.RepositoryNodeInstallSourceResponse + 52, // 314: trpc.ocmanager.repository.RepositoryService.ListRepositoryNodeInstallSources:output_type -> trpc.ocmanager.repository.ListRepositoryNodeInstallSourcesResponse + 56, // 315: trpc.ocmanager.repository.RepositoryService.UpsertRepositoryNodeRegistrationAllowlist:output_type -> trpc.ocmanager.repository.RepositoryNodeRegistrationAllowlistResponse + 58, // 316: trpc.ocmanager.repository.RepositoryService.ListRepositoryNodeRegistrationAllowlists:output_type -> trpc.ocmanager.repository.ListRepositoryNodeRegistrationAllowlistsResponse + 78, // 317: trpc.ocmanager.repository.RepositoryService.SyncRepository:output_type -> trpc.ocmanager.repository.RepositoryTaskResponse + 69, // 318: trpc.ocmanager.repository.RepositoryService.UpsertRepositorySyncSchedule:output_type -> trpc.ocmanager.repository.RepositorySyncScheduleResponse + 69, // 319: trpc.ocmanager.repository.RepositoryService.DescribeRepositorySyncSchedule:output_type -> trpc.ocmanager.repository.RepositorySyncScheduleResponse + 69, // 320: trpc.ocmanager.repository.RepositoryService.PauseRepositorySyncSchedule:output_type -> trpc.ocmanager.repository.RepositorySyncScheduleResponse + 69, // 321: trpc.ocmanager.repository.RepositoryService.ResumeRepositorySyncSchedule:output_type -> trpc.ocmanager.repository.RepositorySyncScheduleResponse + 89, // 322: trpc.ocmanager.repository.RepositoryService.GenerateRepositoryConfig:output_type -> trpc.ocmanager.repository.RepositoryConfigResponse + 94, // 323: trpc.ocmanager.repository.RepositoryService.RegisterRepositoryGPGKey:output_type -> trpc.ocmanager.repository.RepositoryGPGKeyResponse + 95, // 324: trpc.ocmanager.repository.RepositoryService.ListRepositoryGPGKeys:output_type -> trpc.ocmanager.repository.ListRepositoryGPGKeysResponse + 177, // 325: trpc.ocmanager.repository.RepositoryService.DeleteRepositoryGPGKey:output_type -> google.protobuf.Empty + 78, // 326: trpc.ocmanager.repository.RepositoryService.DescribeRepositoryTask:output_type -> trpc.ocmanager.repository.RepositoryTaskResponse + 96, // 327: trpc.ocmanager.repository.RepositoryService.ListRepositoryTasks:output_type -> trpc.ocmanager.repository.ListRepositoryTasksResponse + 78, // 328: trpc.ocmanager.repository.RepositoryService.CancelRepositoryTask:output_type -> trpc.ocmanager.repository.RepositoryTaskResponse + 78, // 329: trpc.ocmanager.repository.RepositoryService.RetryRepositoryTask:output_type -> trpc.ocmanager.repository.RepositoryTaskResponse + 118, // 330: trpc.ocmanager.repository.RepositoryService.ExportRepositoryTaskDiagnosis:output_type -> trpc.ocmanager.repository.RepositoryDiagnosisReportResponse + 159, // 331: trpc.ocmanager.repository.RepositoryService.DescribeRepositoryTaskLog:output_type -> trpc.ocmanager.repository.RepositoryTaskLogResponse + 177, // 332: trpc.ocmanager.repository.RepositoryService.DownloadRepositoryTaskLog:output_type -> google.protobuf.Empty + 85, // 333: trpc.ocmanager.repository.RepositoryService.BuildRepositoryOCAIContext:output_type -> trpc.ocmanager.repository.RepositoryOCAIContextResponse + 86, // 334: trpc.ocmanager.repository.RepositoryService.CreateRepositoryOCAIAnalysis:output_type -> trpc.ocmanager.repository.RepositoryOCAIAnalysisResponse + 86, // 335: trpc.ocmanager.repository.RepositoryService.DescribeRepositoryOCAIAnalysis:output_type -> trpc.ocmanager.repository.RepositoryOCAIAnalysisResponse + 86, // 336: trpc.ocmanager.repository.RepositoryService.AppendRepositoryOCAIFeedback:output_type -> trpc.ocmanager.repository.RepositoryOCAIAnalysisResponse + 107, // 337: trpc.ocmanager.repository.RepositoryService.BeginRepositoryUpload:output_type -> trpc.ocmanager.repository.RepositoryUploadResponse + 107, // 338: trpc.ocmanager.repository.RepositoryService.AppendRepositoryUploadChunk:output_type -> trpc.ocmanager.repository.RepositoryUploadResponse + 102, // 339: trpc.ocmanager.repository.RepositoryService.PutRepositoryUploadChunk:output_type -> trpc.ocmanager.repository.PutRepositoryUploadChunkResponse + 103, // 340: trpc.ocmanager.repository.RepositoryService.DescribeRepositoryUploadChunks:output_type -> trpc.ocmanager.repository.DescribeRepositoryUploadChunksResponse + 107, // 341: trpc.ocmanager.repository.RepositoryService.PauseRepositoryUpload:output_type -> trpc.ocmanager.repository.RepositoryUploadResponse + 107, // 342: trpc.ocmanager.repository.RepositoryService.ResumeRepositoryUpload:output_type -> trpc.ocmanager.repository.RepositoryUploadResponse + 108, // 343: trpc.ocmanager.repository.RepositoryService.CompleteRepositoryUpload:output_type -> trpc.ocmanager.repository.CompleteRepositoryUploadResponse + 107, // 344: trpc.ocmanager.repository.RepositoryService.AbortRepositoryUpload:output_type -> trpc.ocmanager.repository.RepositoryUploadResponse + 107, // 345: trpc.ocmanager.repository.RepositoryService.DescribeRepositoryUpload:output_type -> trpc.ocmanager.repository.RepositoryUploadResponse + 111, // 346: trpc.ocmanager.repository.RepositoryService.ListRepositoryPackages:output_type -> trpc.ocmanager.repository.ListRepositoryPackagesResponse + 114, // 347: trpc.ocmanager.repository.RepositoryService.ListRepositoryQuarantine:output_type -> trpc.ocmanager.repository.ListRepositoryQuarantineResponse + 78, // 348: trpc.ocmanager.repository.RepositoryService.DeleteRepositoryPackage:output_type -> trpc.ocmanager.repository.RepositoryTaskResponse + 134, // 349: trpc.ocmanager.repository.RepositoryService.PreviewRepositoryPackageDeletion:output_type -> trpc.ocmanager.repository.RepositoryPackageDeletionPreviewResponse + 78, // 350: trpc.ocmanager.repository.RepositoryService.ConfirmRepositoryPackageDeletion:output_type -> trpc.ocmanager.repository.RepositoryTaskResponse + 121, // 351: trpc.ocmanager.repository.RepositoryService.ListRepositoryDistributionNodes:output_type -> trpc.ocmanager.repository.ListRepositoryDistributionNodesResponse + 127, // 352: trpc.ocmanager.repository.RepositoryService.CreateRepositoryDistribution:output_type -> trpc.ocmanager.repository.RepositoryDistributionResponse + 127, // 353: trpc.ocmanager.repository.RepositoryService.DescribeRepositoryDistribution:output_type -> trpc.ocmanager.repository.RepositoryDistributionResponse + 128, // 354: trpc.ocmanager.repository.RepositoryService.ListRepositoryDistributions:output_type -> trpc.ocmanager.repository.ListRepositoryDistributionsResponse + 138, // 355: trpc.ocmanager.repository.RepositoryService.PreviewRepositoryPackageInstall:output_type -> trpc.ocmanager.repository.RepositoryPackageInstallPreviewResponse + 142, // 356: trpc.ocmanager.repository.RepositoryService.ConfirmRepositoryPackageInstall:output_type -> trpc.ocmanager.repository.RepositoryPackageInstallResponse + 142, // 357: trpc.ocmanager.repository.RepositoryService.DescribeRepositoryPackageInstall:output_type -> trpc.ocmanager.repository.RepositoryPackageInstallResponse + 146, // 358: trpc.ocmanager.repository.RepositoryService.ExportRepositoryInventory:output_type -> trpc.ocmanager.repository.RepositoryExportResponse + 151, // 359: trpc.ocmanager.repository.RepositoryService.PreviewRepositoryInventoryImport:output_type -> trpc.ocmanager.repository.RepositoryInventoryImportPreviewResponse + 154, // 360: trpc.ocmanager.repository.RepositoryService.ConfirmRepositoryInventoryImport:output_type -> trpc.ocmanager.repository.RepositoryInventoryImportResponse + 146, // 361: trpc.ocmanager.repository.RepositoryService.ExportRepositoryValidationReport:output_type -> trpc.ocmanager.repository.RepositoryExportResponse + 146, // 362: trpc.ocmanager.repository.RepositoryService.ExportRepositoryDistributionResult:output_type -> trpc.ocmanager.repository.RepositoryExportResponse + 78, // 363: trpc.ocmanager.repository.RepositoryService.MigrateRepository:output_type -> trpc.ocmanager.repository.RepositoryTaskResponse + 291, // [291:364] is the sub-list for method output_type + 218, // [218:291] is the sub-list for method input_type + 218, // [218:218] is the sub-list for extension type_name + 218, // [218:218] is the sub-list for extension extendee + 0, // [0:218] is the sub-list for field type_name +} + +func init() { file_repository_repository_proto_init() } +func file_repository_repository_proto_init() { + if File_repository_repository_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_repository_repository_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositorySpec); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Repository); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateRepositoryRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UpdateRepositoryRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DescribeRepositoryRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListRepositoriesRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeleteRepositoryRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*BatchApplyRepositoryItem); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*BatchApplyRepositoriesRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*BatchApplyRepositoryResult); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListRepositoriesResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*BatchApplyRepositoriesResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryStorageNodeSpec); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryStorageNode); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryNodeRepositorySummary); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateRepositoryStorageNodeRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UpdateRepositoryStorageNodeRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DescribeRepositoryStorageNodeRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListRepositoryStorageNodesRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeleteRepositoryStorageNodeRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ProbeRepositoryStorageNodeRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryStorageNodeResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryMetadataSigningKey); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListRepositoryStorageNodesResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryNodeEnrollmentSpec); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryNodeEnrollment); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateRepositoryNodeEnrollmentRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryNodeEnrollmentResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryNodeArtifactSpec); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryNodeArtifact); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RegisterRepositoryNodeArtifactRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryNodeArtifactResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListRepositoryNodeArtifactsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListRepositoryNodeArtifactsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DownloadRepositoryNodeArtifactRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[36].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryNodeInstallSourceSpec); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[37].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryNodeInstallSource); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[38].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ConfigureRepositoryNodeInstallSourceRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[39].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryNodeInstallSourceResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[40].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListRepositoryNodeInstallSourcesRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[41].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListRepositoryNodeInstallSourcesResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[42].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryNodeRegistrationAllowlistSpec); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[43].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryNodeRegistrationAllowlist); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[44].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UpsertRepositoryNodeRegistrationAllowlistRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[45].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryNodeRegistrationAllowlistResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[46].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListRepositoryNodeRegistrationAllowlistsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[47].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListRepositoryNodeRegistrationAllowlistsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[48].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryPreflightRecord); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[49].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryStorageNodeProbeResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[50].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListRepositoryPreflightRecordsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[51].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListRepositoryPreflightRecordsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[52].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SyncRepositoryRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[53].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositorySyncScheduleSpec); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[54].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositorySyncSchedule); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[55].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UpsertRepositorySyncScheduleRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[56].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DescribeRepositorySyncScheduleRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[57].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ChangeRepositorySyncScheduleStatusRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[58].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositorySyncScheduleResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[59].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryTask); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[60].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryTaskResultItem); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[61].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryTaskTarget); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[62].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DescribeRepositoryTaskRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[63].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListRepositoryTasksRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[64].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryTaskStatistics); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[65].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CancelRepositoryTaskRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[66].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RetryRepositoryTaskRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[67].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryTaskResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[68].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryOCAIContext); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[69].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryOCAIAnalysis); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[70].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*BuildRepositoryOCAIContextRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[71].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateRepositoryOCAIAnalysisRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[72].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DescribeRepositoryOCAIAnalysisRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[73].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AppendRepositoryOCAIFeedbackRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[74].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryOCAIContextResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[75].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryOCAIAnalysisResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[76].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GenerateRepositoryConfigRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[77].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[78].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryConfigResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[79].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryGPGKey); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[80].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RegisterRepositoryGPGKeyRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[81].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListRepositoryGPGKeysRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[82].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeleteRepositoryGPGKeyRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[83].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryGPGKeyResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[84].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListRepositoryGPGKeysResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[85].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListRepositoryTasksResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[86].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryUpload); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[87].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*BeginRepositoryUploadRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[88].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AppendRepositoryUploadChunkRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[89].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PutRepositoryUploadChunkRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[90].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryUploadChunk); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[91].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PutRepositoryUploadChunkResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[92].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DescribeRepositoryUploadChunksResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[93].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CompleteRepositoryUploadRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[94].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AbortRepositoryUploadRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[95].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DescribeRepositoryUploadRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[96].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryUploadResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[97].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CompleteRepositoryUploadResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[98].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryPackage); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[99].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListRepositoryPackagesRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[100].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListRepositoryPackagesResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[101].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryQuarantineItem); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[102].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListRepositoryQuarantineRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[103].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListRepositoryQuarantineResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[104].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeleteRepositoryPackageRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[105].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryDiagnosis); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[106].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ExportRepositoryTaskDiagnosisRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[107].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryDiagnosisReportResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[108].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryDistributionNode); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[109].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListRepositoryDistributionNodesRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[110].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListRepositoryDistributionNodesResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[111].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateRepositoryDistributionRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[112].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DescribeRepositoryDistributionRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[113].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListRepositoryDistributionsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[114].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryDistributionTarget); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[115].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryDistribution); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[116].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryDistributionResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[117].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListRepositoryDistributionsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[118].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PreviewRepositoryPackageDeletionRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[119].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ConfirmRepositoryPackageDeletionRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[120].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryPackageDeletionPreviewSource); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[121].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryPackageDependencyImpact); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[122].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryPackageDeletionPreview); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[123].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryPackageDeletionPreviewResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[124].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PreviewRepositoryPackageInstallRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[125].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryPackageInstallPreviewTarget); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[126].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryPackageInstallPreview); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[127].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryPackageInstallPreviewResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[128].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ConfirmRepositoryPackageInstallRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[129].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DescribeRepositoryPackageInstallRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[130].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryPackageInstall); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[131].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryPackageInstallResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[132].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ExportRepositoryInventoryRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[133].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ExportRepositoryValidationReportRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[134].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ExportRepositoryDistributionResultRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[135].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryExportResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[136].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryInventoryStorageNodeMapping); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[137].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryInventorySigningKeyMapping); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[138].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PreviewRepositoryInventoryImportRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[139].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryInventoryImportItem); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[140].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryInventoryImportPreviewResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[141].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ConfirmRepositoryInventoryImportRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[142].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryInventoryImportJob); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[143].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryInventoryImportResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[144].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MigrateRepositoryRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[145].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DescribeRepositoryTaskLogRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[146].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DownloadRepositoryTaskLogRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[147].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryTaskLog); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[148].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryTaskLogResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[149].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryUpstreamProfileSpec); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[150].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryUpstreamProfile); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[151].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateRepositoryUpstreamProfileRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[152].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UpdateRepositoryUpstreamProfileRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[153].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DescribeRepositoryUpstreamProfileRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[154].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListRepositoryUpstreamProfilesRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[155].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeleteRepositoryUpstreamProfileRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[156].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryUpstreamProfileResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_repository_proto_msgTypes[157].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListRepositoryUpstreamProfilesResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_repository_repository_proto_msgTypes[0].OneofWrappers = []interface{}{} + file_repository_repository_proto_msgTypes[13].OneofWrappers = []interface{}{} + file_repository_repository_proto_msgTypes[149].OneofWrappers = []interface{}{} + file_repository_repository_proto_msgTypes[154].OneofWrappers = []interface{}{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_repository_repository_proto_rawDesc, + NumEnums: 11, + NumMessages: 163, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_repository_repository_proto_goTypes, + DependencyIndexes: file_repository_repository_proto_depIdxs, + EnumInfos: file_repository_repository_proto_enumTypes, + MessageInfos: file_repository_repository_proto_msgTypes, + }.Build() + File_repository_repository_proto = out.File + file_repository_repository_proto_rawDesc = nil + file_repository_repository_proto_goTypes = nil + file_repository_repository_proto_depIdxs = nil +} diff --git a/manager/backend/protocol/repository/repository.proto b/manager/backend/protocol/repository/repository.proto new file mode 100644 index 0000000000000000000000000000000000000000..2f211054b16c42c0712e8afd3a23a23b340de11b --- /dev/null +++ b/manager/backend/protocol/repository/repository.proto @@ -0,0 +1,1469 @@ +syntax = "proto3"; + +package trpc.ocmanager.repository; + +option go_package = "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository"; + +import "common/common.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; +import "validate.proto"; + +// RepositoryService manages software repository definitions. Content mutation +// and publication are handled by later phases through immutable revisions. +service RepositoryService { + rpc CreateRepository(CreateRepositoryRequest) returns (RepositoryResponse); + rpc UpdateRepository(UpdateRepositoryRequest) returns (RepositoryResponse); + rpc DescribeRepository(DescribeRepositoryRequest) returns (RepositoryResponse); + rpc ListRepositories(ListRepositoriesRequest) returns (ListRepositoriesResponse); + rpc DeleteRepository(DeleteRepositoryRequest) returns (google.protobuf.Empty); + rpc BatchApplyRepositories(BatchApplyRepositoriesRequest) returns (BatchApplyRepositoriesResponse); + rpc CreateRepositoryUpstreamProfile(CreateRepositoryUpstreamProfileRequest) returns (RepositoryUpstreamProfileResponse); + rpc UpdateRepositoryUpstreamProfile(UpdateRepositoryUpstreamProfileRequest) returns (RepositoryUpstreamProfileResponse); + rpc DescribeRepositoryUpstreamProfile(DescribeRepositoryUpstreamProfileRequest) returns (RepositoryUpstreamProfileResponse); + rpc ListRepositoryUpstreamProfiles(ListRepositoryUpstreamProfilesRequest) returns (ListRepositoryUpstreamProfilesResponse); + rpc DeleteRepositoryUpstreamProfile(DeleteRepositoryUpstreamProfileRequest) returns (google.protobuf.Empty); + rpc CreateRepositoryStorageNode(CreateRepositoryStorageNodeRequest) returns (RepositoryStorageNodeResponse); + rpc UpdateRepositoryStorageNode(UpdateRepositoryStorageNodeRequest) returns (RepositoryStorageNodeResponse); + rpc DescribeRepositoryStorageNode(DescribeRepositoryStorageNodeRequest) returns (RepositoryStorageNodeResponse); + rpc ListRepositoryStorageNodes(ListRepositoryStorageNodesRequest) returns (ListRepositoryStorageNodesResponse); + rpc DeleteRepositoryStorageNode(DeleteRepositoryStorageNodeRequest) returns (google.protobuf.Empty); + rpc ProbeRepositoryStorageNode(ProbeRepositoryStorageNodeRequest) returns (RepositoryStorageNodeProbeResponse); + rpc ListRepositoryPreflightRecords(ListRepositoryPreflightRecordsRequest) returns (ListRepositoryPreflightRecordsResponse); + rpc CreateRepositoryNodeEnrollment(CreateRepositoryNodeEnrollmentRequest) returns (RepositoryNodeEnrollmentResponse); + rpc RegisterRepositoryNodeArtifact(RegisterRepositoryNodeArtifactRequest) returns (RepositoryNodeArtifactResponse); + rpc ListRepositoryNodeArtifacts(ListRepositoryNodeArtifactsRequest) returns (ListRepositoryNodeArtifactsResponse); + rpc DownloadRepositoryNodeArtifact(DownloadRepositoryNodeArtifactRequest) returns (google.protobuf.Empty); + rpc ConfigureRepositoryNodeInstallSource(ConfigureRepositoryNodeInstallSourceRequest) returns (RepositoryNodeInstallSourceResponse); + rpc ListRepositoryNodeInstallSources(ListRepositoryNodeInstallSourcesRequest) returns (ListRepositoryNodeInstallSourcesResponse); + rpc UpsertRepositoryNodeRegistrationAllowlist(UpsertRepositoryNodeRegistrationAllowlistRequest) returns (RepositoryNodeRegistrationAllowlistResponse); + rpc ListRepositoryNodeRegistrationAllowlists(ListRepositoryNodeRegistrationAllowlistsRequest) returns (ListRepositoryNodeRegistrationAllowlistsResponse); + rpc SyncRepository(SyncRepositoryRequest) returns (RepositoryTaskResponse); + rpc UpsertRepositorySyncSchedule(UpsertRepositorySyncScheduleRequest) returns (RepositorySyncScheduleResponse); + rpc DescribeRepositorySyncSchedule(DescribeRepositorySyncScheduleRequest) returns (RepositorySyncScheduleResponse); + rpc PauseRepositorySyncSchedule(ChangeRepositorySyncScheduleStatusRequest) returns (RepositorySyncScheduleResponse); + rpc ResumeRepositorySyncSchedule(ChangeRepositorySyncScheduleStatusRequest) returns (RepositorySyncScheduleResponse); + rpc GenerateRepositoryConfig(GenerateRepositoryConfigRequest) returns (RepositoryConfigResponse); + rpc RegisterRepositoryGPGKey(RegisterRepositoryGPGKeyRequest) returns (RepositoryGPGKeyResponse); + rpc ListRepositoryGPGKeys(ListRepositoryGPGKeysRequest) returns (ListRepositoryGPGKeysResponse); + rpc DeleteRepositoryGPGKey(DeleteRepositoryGPGKeyRequest) returns (google.protobuf.Empty); + rpc DescribeRepositoryTask(DescribeRepositoryTaskRequest) returns (RepositoryTaskResponse); + rpc ListRepositoryTasks(ListRepositoryTasksRequest) returns (ListRepositoryTasksResponse); + rpc CancelRepositoryTask(CancelRepositoryTaskRequest) returns (RepositoryTaskResponse); + rpc RetryRepositoryTask(RetryRepositoryTaskRequest) returns (RepositoryTaskResponse); + rpc ExportRepositoryTaskDiagnosis(ExportRepositoryTaskDiagnosisRequest) returns (RepositoryDiagnosisReportResponse); + rpc DescribeRepositoryTaskLog(DescribeRepositoryTaskLogRequest) returns (RepositoryTaskLogResponse); + rpc DownloadRepositoryTaskLog(DownloadRepositoryTaskLogRequest) returns (google.protobuf.Empty); + rpc BuildRepositoryOCAIContext(BuildRepositoryOCAIContextRequest) returns (RepositoryOCAIContextResponse); + rpc CreateRepositoryOCAIAnalysis(CreateRepositoryOCAIAnalysisRequest) returns (RepositoryOCAIAnalysisResponse); + rpc DescribeRepositoryOCAIAnalysis(DescribeRepositoryOCAIAnalysisRequest) returns (RepositoryOCAIAnalysisResponse); + rpc AppendRepositoryOCAIFeedback(AppendRepositoryOCAIFeedbackRequest) returns (RepositoryOCAIAnalysisResponse); + rpc BeginRepositoryUpload(BeginRepositoryUploadRequest) returns (RepositoryUploadResponse); + rpc AppendRepositoryUploadChunk(AppendRepositoryUploadChunkRequest) returns (RepositoryUploadResponse); + rpc PutRepositoryUploadChunk(PutRepositoryUploadChunkRequest) returns (PutRepositoryUploadChunkResponse); + rpc DescribeRepositoryUploadChunks(DescribeRepositoryUploadRequest) returns (DescribeRepositoryUploadChunksResponse); + rpc PauseRepositoryUpload(DescribeRepositoryUploadRequest) returns (RepositoryUploadResponse); + rpc ResumeRepositoryUpload(DescribeRepositoryUploadRequest) returns (RepositoryUploadResponse); + rpc CompleteRepositoryUpload(CompleteRepositoryUploadRequest) returns (CompleteRepositoryUploadResponse); + rpc AbortRepositoryUpload(AbortRepositoryUploadRequest) returns (RepositoryUploadResponse); + rpc DescribeRepositoryUpload(DescribeRepositoryUploadRequest) returns (RepositoryUploadResponse); + rpc ListRepositoryPackages(ListRepositoryPackagesRequest) returns (ListRepositoryPackagesResponse); + rpc ListRepositoryQuarantine(ListRepositoryQuarantineRequest) returns (ListRepositoryQuarantineResponse); + rpc DeleteRepositoryPackage(DeleteRepositoryPackageRequest) returns (RepositoryTaskResponse); + rpc PreviewRepositoryPackageDeletion(PreviewRepositoryPackageDeletionRequest) returns (RepositoryPackageDeletionPreviewResponse); + rpc ConfirmRepositoryPackageDeletion(ConfirmRepositoryPackageDeletionRequest) returns (RepositoryTaskResponse); + rpc ListRepositoryDistributionNodes(ListRepositoryDistributionNodesRequest) returns (ListRepositoryDistributionNodesResponse); + rpc CreateRepositoryDistribution(CreateRepositoryDistributionRequest) returns (RepositoryDistributionResponse); + rpc DescribeRepositoryDistribution(DescribeRepositoryDistributionRequest) returns (RepositoryDistributionResponse); + rpc ListRepositoryDistributions(ListRepositoryDistributionsRequest) returns (ListRepositoryDistributionsResponse); + rpc PreviewRepositoryPackageInstall(PreviewRepositoryPackageInstallRequest) returns (RepositoryPackageInstallPreviewResponse); + rpc ConfirmRepositoryPackageInstall(ConfirmRepositoryPackageInstallRequest) returns (RepositoryPackageInstallResponse); + rpc DescribeRepositoryPackageInstall(DescribeRepositoryPackageInstallRequest) returns (RepositoryPackageInstallResponse); + rpc ExportRepositoryInventory(ExportRepositoryInventoryRequest) returns (RepositoryExportResponse); + rpc PreviewRepositoryInventoryImport(PreviewRepositoryInventoryImportRequest) returns (RepositoryInventoryImportPreviewResponse); + rpc ConfirmRepositoryInventoryImport(ConfirmRepositoryInventoryImportRequest) returns (RepositoryInventoryImportResponse); + rpc ExportRepositoryValidationReport(ExportRepositoryValidationReportRequest) returns (RepositoryExportResponse); + rpc ExportRepositoryDistributionResult(ExportRepositoryDistributionResultRequest) returns (RepositoryExportResponse); + rpc MigrateRepository(MigrateRepositoryRequest) returns (RepositoryTaskResponse); +} + +enum RepositoryType { + REPOSITORY_TYPE_UNSPECIFIED = 0; + REPOSITORY_TYPE_BASELINE = 1; + REPOSITORY_TYPE_UPDATE = 2; + REPOSITORY_TYPE_PRIVATE = 3; +} + +enum RepositoryArchitecture { + REPOSITORY_ARCHITECTURE_UNSPECIFIED = 0; + REPOSITORY_ARCHITECTURE_X86_64 = 1; + REPOSITORY_ARCHITECTURE_AARCH64 = 2; + REPOSITORY_ARCHITECTURE_NOARCH = 3; +} + +enum RepositoryGPGPolicy { + REPOSITORY_GPG_POLICY_UNSPECIFIED = 0; + REPOSITORY_GPG_POLICY_DISABLED = 1; + // Deprecated compatibility value. The behavior is identical to ENFORCE. + REPOSITORY_GPG_POLICY_VERIFY_PACKAGE = 2; + REPOSITORY_GPG_POLICY_AUDIT = 3; + REPOSITORY_GPG_POLICY_ENFORCE = 4; +} + +enum RepositorySyncMode { + REPOSITORY_SYNC_MODE_UNSPECIFIED = 0; + REPOSITORY_SYNC_MODE_MIRROR = 1; + REPOSITORY_SYNC_MODE_APPEND = 2; +} + +enum RepositoryInventoryConflictStrategy { + REPOSITORY_INVENTORY_CONFLICT_STRATEGY_UNSPECIFIED = 0; + REPOSITORY_INVENTORY_CONFLICT_STRATEGY_FAIL = 1; + REPOSITORY_INVENTORY_CONFLICT_STRATEGY_SKIP = 2; + REPOSITORY_INVENTORY_CONFLICT_STRATEGY_RENAME = 3; +} + +enum RepositorySyncScheduleCadence { + REPOSITORY_SYNC_SCHEDULE_CADENCE_UNSPECIFIED = 0; + REPOSITORY_SYNC_SCHEDULE_CADENCE_DAILY = 1; + REPOSITORY_SYNC_SCHEDULE_CADENCE_WEEKLY = 2; +} + +enum RepositorySyncScheduleStatus { + REPOSITORY_SYNC_SCHEDULE_STATUS_UNSPECIFIED = 0; + REPOSITORY_SYNC_SCHEDULE_STATUS_ENABLED = 1; + REPOSITORY_SYNC_SCHEDULE_STATUS_PAUSED = 2; + REPOSITORY_SYNC_SCHEDULE_STATUS_AUTO_PAUSED = 3; +} + +enum RepositoryTaskType { + REPOSITORY_TASK_TYPE_UNSPECIFIED = 0; + REPOSITORY_TASK_TYPE_SYNC = 1; + REPOSITORY_TASK_TYPE_UPLOAD = 2; + REPOSITORY_TASK_TYPE_DELETE_PACKAGE = 3; + REPOSITORY_TASK_TYPE_REBUILD_METADATA = 4; + REPOSITORY_TASK_TYPE_DELETE_REPOSITORY = 5; + REPOSITORY_TASK_TYPE_PUBLISH = 6; + REPOSITORY_TASK_TYPE_VALIDATE = 7; + REPOSITORY_TASK_TYPE_DISTRIBUTE = 8; + REPOSITORY_TASK_TYPE_MIGRATE_REPOSITORY = 9; + REPOSITORY_TASK_TYPE_NODE_INSTALL = 10; + REPOSITORY_TASK_TYPE_PACKAGE_INSTALL = 11; + REPOSITORY_TASK_TYPE_INVENTORY_IMPORT = 12; +} + +enum RepositoryTaskStatus { + REPOSITORY_TASK_STATUS_UNSPECIFIED = 0; + REPOSITORY_TASK_STATUS_PENDING = 1; + REPOSITORY_TASK_STATUS_RUNNING = 2; + REPOSITORY_TASK_STATUS_CANCEL_REQUESTED = 3; + REPOSITORY_TASK_STATUS_CANCELED = 4; + REPOSITORY_TASK_STATUS_SUCCESS = 5; + REPOSITORY_TASK_STATUS_FAILED = 6; + REPOSITORY_TASK_STATUS_PARTIAL_SUCCESS = 7; +} + +message RepositorySpec { + int64 project_id = 1 [(validate.rules).int64 = {gt: 0}]; + string system_product = 2 [(validate.rules).string = {min_len: 1, max_len: 128}]; + string product_version = 3 [(validate.rules).string = {min_len: 1, max_len: 64}]; + string name = 4 [(validate.rules).string = {min_len: 1, max_len: 128}]; + string description = 5 [(validate.rules).string = {max_len: 1024}]; + RepositoryArchitecture architecture = 6; + RepositoryType type = 7; + string upstream_url = 8 [(validate.rules).string = {max_len: 2048}]; + bool allow_upload = 9; + repeated string include_packages = 10; + repeated string exclude_packages = 11; + RepositoryGPGPolicy gpg_policy = 12; + optional bool enabled = 13; + string primary_storage_node_id = 14 [(validate.rules).string = {min_len: 1, max_len: 64}]; + RepositorySyncMode sync_mode = 15; + int64 bandwidth_limit_bytes = 16 [(validate.rules).int64 = {gte: 0}]; + int32 max_parallel_downloads = 17 [(validate.rules).int32 = {gte: 0, lte: 20}]; + string metadata_signing_key_ref = 18 [(validate.rules).string = {max_len: 128}]; +} + +message Repository { + string id = 1; + int64 project_id = 2; + string system_product = 3; + string product_version = 4; + string name = 5; + string description = 6; + RepositoryArchitecture architecture = 7; + RepositoryType type = 8; + string upstream_url = 9; + string storage_location = 10; + string publish_key = 11; + bool allow_upload = 12; + repeated string include_packages = 13; + repeated string exclude_packages = 14; + RepositoryGPGPolicy gpg_policy = 15; + bool enabled = 16; + string status = 17; + string active_revision_id = 18; + string creator = 19; + google.protobuf.Timestamp created_at = 20; + google.protobuf.Timestamp updated_at = 21; + string primary_storage_node_id = 22; + string publish_base_url_snapshot = 23; + int64 package_count = 24; + int64 logical_bytes = 25; + google.protobuf.Timestamp last_sync_at = 26; + RepositoryTaskStatus last_sync_status = 27; + RepositorySyncMode sync_mode = 28; + int64 bandwidth_limit_bytes = 29; + int32 max_parallel_downloads = 30; + string metadata_signing_key_ref = 31; + int64 revision_count = 32; +} + +message CreateRepositoryRequest { + RepositorySpec spec = 1; +} + +message UpdateRepositoryRequest { + string repository_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + RepositorySpec spec = 2; +} + +message DescribeRepositoryRequest { + string repository_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} + +message ListRepositoriesRequest { + int64 project_id = 1 [(validate.rules).int64 = {gt: 0}]; + string system_product = 2 [(validate.rules).string = {max_len: 128}]; + string product_version = 3 [(validate.rules).string = {max_len: 64}]; + RepositoryArchitecture architecture = 4; + RepositoryType type = 5; + string keyword = 6 [(validate.rules).string = {max_len: 128}]; + common.PageRequest page_request = 10; +} + +message DeleteRepositoryRequest { + string repository_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + bool confirm_delete = 2; +} + +message BatchApplyRepositoryItem { + // Empty repository_id creates a repository; a non-empty ID updates it. + string repository_id = 1 [(validate.rules).string = {max_len: 64}]; + RepositorySpec spec = 2; +} + +message BatchApplyRepositoriesRequest { + repeated BatchApplyRepositoryItem items = 1; +} + +message BatchApplyRepositoryResult { + int32 input_index = 1; + string operation = 2; + Repository repository = 3; +} + +message RepositoryResponse { + Repository repository = 1; +} + +message ListRepositoriesResponse { + repeated Repository repositories = 1; + common.PageResponse page_response = 10; +} + +message BatchApplyRepositoriesResponse { + repeated BatchApplyRepositoryResult results = 1; +} + +enum RepositoryStorageNodeStatus { + REPOSITORY_STORAGE_NODE_STATUS_UNSPECIFIED = 0; + REPOSITORY_STORAGE_NODE_STATUS_UNKNOWN = 1; + REPOSITORY_STORAGE_NODE_STATUS_ONLINE = 2; + REPOSITORY_STORAGE_NODE_STATUS_OFFLINE = 3; + REPOSITORY_STORAGE_NODE_STATUS_DISABLED = 4; + REPOSITORY_STORAGE_NODE_STATUS_PENDING = 5; + REPOSITORY_STORAGE_NODE_STATUS_INSTALLING = 6; + REPOSITORY_STORAGE_NODE_STATUS_INSTALL_FAILED = 7; +} + +message RepositoryStorageNodeSpec { + int64 project_id = 1 [(validate.rules).int64 = {gt: 0}]; + string name = 2 [(validate.rules).string = {min_len: 1, max_len: 128}]; + string description = 3 [(validate.rules).string = {max_len: 1024}]; + string control_base_url = 4 [(validate.rules).string = {min_len: 1, max_len: 2048}]; + string publish_base_url = 5 [(validate.rules).string = {min_len: 1, max_len: 2048}]; + string tls_server_name = 6 [(validate.rules).string = {max_len: 255}]; + string certificate_fingerprint = 7 [(validate.rules).string = {min_len: 64, max_len: 95}]; + optional bool enabled = 8; + map labels = 9; +} + +message RepositoryStorageNode { + string id = 1; + int64 project_id = 2; + string name = 3; + string description = 4; + string control_base_url = 5; + string publish_base_url = 6; + string tls_server_name = 7; + string certificate_fingerprint = 8; + RepositoryStorageNodeStatus status = 9; + bool enabled = 10; + string filesystem_type = 11; + int64 capacity_bytes = 12; + int64 free_bytes = 13; + int64 free_inodes = 14; + map tool_versions = 15; + map labels = 16; + google.protobuf.Timestamp last_heartbeat_at = 17; + google.protobuf.Timestamp last_probe_at = 18; + string last_error_code = 19; + string last_error_message = 20; + string created_by = 21; + google.protobuf.Timestamp created_at = 22; + google.protobuf.Timestamp updated_at = 23; + string hostname = 24; + string ip_address = 25; + string architecture = 26; + string os_name = 27; + string os_version = 28; + string agent_version = 29; + int64 disk_used_bytes = 30; + double disk_usage_percent = 31; + int64 repository_count = 32; + int64 repository_physical_bytes = 33; + string error_zh_message = 34; + string error_zh_recommended_action = 35; +} + +message RepositoryNodeRepositorySummary { + string repository_id = 1; + string name = 2; + string system_product = 3; + string product_version = 4; + RepositoryType repository_type = 5; + RepositoryArchitecture architecture = 6; + string upstream_url = 7; + int64 package_count = 8; + int64 logical_bytes = 9; + int64 physical_bytes = 10; + string current_revision = 11; + string publish_status = 12; + google.protobuf.Timestamp last_sync_at = 13; + RepositoryTaskStatus last_sync_status = 14; + google.protobuf.Timestamp next_sync_at = 15; + string running_task_id = 16; + string last_sync_error_code = 17; + string last_sync_error_message = 18; + string last_sync_error_zh_message = 19; + string last_sync_error_zh_recommended_action = 20; +} + +message CreateRepositoryStorageNodeRequest { RepositoryStorageNodeSpec spec = 1; } + +message UpdateRepositoryStorageNodeRequest { + string storage_node_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + RepositoryStorageNodeSpec spec = 2; +} + +message DescribeRepositoryStorageNodeRequest { + string storage_node_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} + +message ListRepositoryStorageNodesRequest { + int64 project_id = 1 [(validate.rules).int64 = {gt: 0}]; + RepositoryStorageNodeStatus status = 2; + string keyword = 3 [(validate.rules).string = {max_len: 128}]; + common.PageRequest page_request = 10; +} + +message DeleteRepositoryStorageNodeRequest { + string storage_node_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + bool confirm_delete = 2; +} + +message ProbeRepositoryStorageNodeRequest { + string storage_node_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} + +message RepositoryStorageNodeResponse { + RepositoryStorageNode storage_node = 1; + repeated RepositoryNodeRepositorySummary repositories = 2; + repeated RepositoryMetadataSigningKey metadata_signing_keys = 3; +} + +message RepositoryMetadataSigningKey { + string id = 1; + string storage_node_id = 2; + string key_ref = 3; + string fingerprint = 4; + string public_key_name = 5; + string public_key_url = 6; + string status = 7; + google.protobuf.Timestamp key_created_at = 8; + google.protobuf.Timestamp observed_at = 9; +} + +message ListRepositoryStorageNodesResponse { + repeated RepositoryStorageNode storage_nodes = 1; + int64 total_nodes = 2; + int64 online_nodes = 3; + int64 total_capacity_bytes = 4; + int64 used_capacity_bytes = 5; + int64 critical_nodes = 6; + common.PageResponse page_response = 10; +} + +enum RepositoryNodeEnrollmentStatus { + REPOSITORY_NODE_ENROLLMENT_STATUS_UNSPECIFIED = 0; + REPOSITORY_NODE_ENROLLMENT_STATUS_PENDING = 1; + REPOSITORY_NODE_ENROLLMENT_STATUS_INSTALLING = 2; + REPOSITORY_NODE_ENROLLMENT_STATUS_BOOTSTRAPPED = 3; + REPOSITORY_NODE_ENROLLMENT_STATUS_ONLINE = 4; + REPOSITORY_NODE_ENROLLMENT_STATUS_FAILED = 5; + REPOSITORY_NODE_ENROLLMENT_STATUS_EXPIRED = 6; + REPOSITORY_NODE_ENROLLMENT_STATUS_AUTHORIZED = 7; + REPOSITORY_NODE_ENROLLMENT_STATUS_REGISTERING = 8; + REPOSITORY_NODE_ENROLLMENT_STATUS_REGISTERED = 9; +} + +message RepositoryNodeEnrollmentSpec { + int64 project_id = 1 [(validate.rules).int64 = {gt: 0}]; + string name = 2 [(validate.rules).string = {min_len: 1, max_len: 128}]; + string description = 3 [(validate.rules).string = {max_len: 1024}]; + string control_base_url = 4 [(validate.rules).string = {min_len: 1, max_len: 2048}]; + string publish_base_url = 5 [(validate.rules).string = {min_len: 1, max_len: 2048}]; + string tls_server_name = 6 [(validate.rules).string = {max_len: 255}]; + map labels = 7; + RepositoryArchitecture expected_architecture = 8; + uint64 tms_agent_id = 9; + string artifact_id = 10 [(validate.rules).string = {max_len: 64}]; +} + +message RepositoryNodeEnrollment { + string id = 1; + string storage_node_id = 2; + int64 project_id = 3; + uint64 tms_agent_id = 4; + int32 generation = 5; + RepositoryNodeEnrollmentStatus status = 6; + string install_task_id = 7; + RepositoryArchitecture expected_architecture = 8; + google.protobuf.Timestamp expires_at = 9; + string last_error_code = 10; + string last_error_message = 11; + string created_by = 12; + google.protobuf.Timestamp created_at = 13; + google.protobuf.Timestamp updated_at = 14; + string tms_task_id = 15; + string allowlist_entry_id = 16; + string artifact_id = 17; + string authorization_id = 18; + string csr_sha256 = 19; + string issuance_status = 20; +} + +message CreateRepositoryNodeEnrollmentRequest { RepositoryNodeEnrollmentSpec spec = 1; } + +message RepositoryNodeEnrollmentResponse { + RepositoryStorageNode storage_node = 1; + RepositoryNodeEnrollment enrollment = 2; + RepositoryTask task = 3; + RepositoryTaskTarget target = 4; + string bootstrap_token = 5; + google.protobuf.Timestamp bootstrap_expires_at = 6; +} + +message RepositoryNodeArtifactSpec { + string name = 1 [(validate.rules).string = {min_len: 1, max_len: 128}]; + string version = 2 [(validate.rules).string = {min_len: 1, max_len: 64}]; + string release = 3 [(validate.rules).string = {min_len: 1, max_len: 32}]; + RepositoryArchitecture architecture = 4; + string file_name = 5 [(validate.rules).string = {min_len: 1, max_len: 255}]; + int64 size_bytes = 6 [(validate.rules).int64 = {gt: 0}]; + string sha256 = 7 [(validate.rules).string = {len: 64}]; + string build_commit = 8 [(validate.rules).string = {min_len: 7, max_len: 64}]; + string relative_path = 9 [(validate.rules).string = {min_len: 1, max_len: 512}]; + int64 project_id = 10 [(validate.rules).int64 = {gte: 0}]; + string source_repository_id = 11 [(validate.rules).string = {max_len: 64}]; + string revision_id = 12 [(validate.rules).string = {max_len: 64}]; + string package_id = 13 [(validate.rules).string = {max_len: 64}]; +} + +message RepositoryNodeArtifact { + string id = 1; + string name = 2; + string version = 3; + string release = 4; + RepositoryArchitecture architecture = 5; + string file_name = 6; + int64 size_bytes = 7; + string sha256 = 8; + string build_commit = 9; + string relative_path = 10; + string status = 11; + google.protobuf.Timestamp created_at = 12; + int64 project_id = 13; + string source_kind = 14; + string source_repository_id = 15; + string revision_id = 16; + string package_id = 17; + uint64 epoch = 18; + string nevra = 19; + string immutable_url = 20; +} + +message RegisterRepositoryNodeArtifactRequest { RepositoryNodeArtifactSpec spec = 1; } +message RepositoryNodeArtifactResponse { RepositoryNodeArtifact artifact = 1; } +message ListRepositoryNodeArtifactsRequest { + string status = 1 [(validate.rules).string = {max_len: 32}]; + int64 project_id = 2 [(validate.rules).int64 = {gt: 0}]; +} +message ListRepositoryNodeArtifactsResponse { repeated RepositoryNodeArtifact artifacts = 1; } +message DownloadRepositoryNodeArtifactRequest { + string artifact_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} + +message RepositoryNodeInstallSourceSpec { + int64 project_id = 1 [(validate.rules).int64 = {gt: 0}]; + string repository_id = 2 [(validate.rules).string = {min_len: 36, max_len: 64}]; + RepositoryArchitecture architecture = 3; + string status = 4 [(validate.rules).string = {max_len: 32}]; +} + +message RepositoryNodeInstallSource { + string id = 1; + int64 project_id = 2; + string repository_id = 3; + RepositoryArchitecture architecture = 4; + string status = 5; + string created_by = 6; + string updated_by = 7; + google.protobuf.Timestamp created_at = 8; + google.protobuf.Timestamp updated_at = 9; +} + +message ConfigureRepositoryNodeInstallSourceRequest { RepositoryNodeInstallSourceSpec spec = 1; } +message RepositoryNodeInstallSourceResponse { RepositoryNodeInstallSource install_source = 1; } +message ListRepositoryNodeInstallSourcesRequest { int64 project_id = 1 [(validate.rules).int64 = {gt: 0}]; } +message ListRepositoryNodeInstallSourcesResponse { repeated RepositoryNodeInstallSource install_sources = 1; } + +message RepositoryNodeRegistrationAllowlistSpec { + int64 project_id = 1 [(validate.rules).int64 = {gt: 0}]; + uint64 tms_agent_id = 2 [(validate.rules).uint64 = {gt: 0}]; + string expected_hostname = 3 [(validate.rules).string = {max_len: 255}]; + string expected_ip = 4 [(validate.rules).string = {max_len: 64}]; + RepositoryArchitecture expected_architecture = 5; + string status = 6 [(validate.rules).string = {max_len: 32}]; + google.protobuf.Timestamp expires_at = 7; +} + +message RepositoryNodeRegistrationAllowlist { + string id = 1; + int64 project_id = 2; + uint64 tms_agent_id = 3; + string expected_hostname = 4; + string expected_ip = 5; + RepositoryArchitecture expected_architecture = 6; + string status = 7; + google.protobuf.Timestamp expires_at = 8; + string created_by = 9; + string updated_by = 10; + google.protobuf.Timestamp created_at = 11; + google.protobuf.Timestamp updated_at = 12; +} + +message UpsertRepositoryNodeRegistrationAllowlistRequest { RepositoryNodeRegistrationAllowlistSpec spec = 1; } +message RepositoryNodeRegistrationAllowlistResponse { RepositoryNodeRegistrationAllowlist allowlist = 1; } +message ListRepositoryNodeRegistrationAllowlistsRequest { int64 project_id = 1 [(validate.rules).int64 = {gt: 0}]; } +message ListRepositoryNodeRegistrationAllowlistsResponse { repeated RepositoryNodeRegistrationAllowlist allowlists = 1; } + +message RepositoryPreflightRecord { + string id = 1; + string storage_node_id = 2; + string repository_id = 3; + string task_id = 4; + string status = 5; + string filesystem_type = 6; + int64 capacity_bytes = 7; + int64 free_bytes = 8; + int64 required_free_bytes = 9; + int64 free_inodes = 10; + int64 required_inodes = 11; + int64 estimated_package_count = 12; + int64 estimated_import_bytes = 13; + map tool_versions = 14; + string error_code = 15; + string error_message = 16; + google.protobuf.Timestamp expires_at = 17; + google.protobuf.Timestamp created_at = 18; +} + +message RepositoryStorageNodeProbeResponse { + RepositoryStorageNode storage_node = 1; + RepositoryPreflightRecord preflight = 2; +} + +message ListRepositoryPreflightRecordsRequest { + string storage_node_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + string repository_id = 2 [(validate.rules).string = {max_len: 64}]; + common.PageRequest page_request = 10; +} + +message ListRepositoryPreflightRecordsResponse { + repeated RepositoryPreflightRecord records = 1; + common.PageResponse page_response = 10; +} + +message SyncRepositoryRequest { + string repository_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} + +message RepositorySyncScheduleSpec { + RepositorySyncScheduleCadence cadence = 1; + string timezone = 2 [(validate.rules).string = {min_len: 1, max_len: 64}]; + int32 day_of_week = 3 [(validate.rules).int32 = {gte: 0, lte: 6}]; + int32 hour = 4 [(validate.rules).int32 = {gte: 0, lte: 23}]; + int32 minute = 5 [(validate.rules).int32 = {gte: 0, lte: 59}]; + int32 failure_threshold = 6 [(validate.rules).int32 = {gte: 1, lte: 100}]; +} + +message RepositorySyncSchedule { + string id = 1; + string repository_id = 2; + int64 project_id = 3; + RepositorySyncScheduleCadence cadence = 4; + string timezone = 5; + int32 day_of_week = 6; + int32 hour = 7; + int32 minute = 8; + RepositorySyncScheduleStatus status = 9; + google.protobuf.Timestamp last_run_at = 10; + google.protobuf.Timestamp next_run_at = 11; + int32 consecutive_failures = 12; + int32 failure_threshold = 13; + string last_error_code = 14; + string last_error_message = 15; + string created_by = 16; + google.protobuf.Timestamp created_at = 17; + google.protobuf.Timestamp updated_at = 18; +} + +message UpsertRepositorySyncScheduleRequest { + string repository_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + RepositorySyncScheduleSpec spec = 2; +} + +message DescribeRepositorySyncScheduleRequest { + string repository_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} + +message ChangeRepositorySyncScheduleStatusRequest { + string repository_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} + +message RepositorySyncScheduleResponse { + RepositorySyncSchedule schedule = 1; +} + +message RepositoryTask { + string id = 1; + string root_task_id = 2; + string retry_of_task_id = 3; + string repository_id = 4; + int64 project_id = 5; + string revision_id = 6; + RepositoryTaskType type = 7; + RepositoryTaskStatus status = 8; + bool requires_mutation_lock = 9; + int32 attempt = 10; + int32 max_attempts = 11; + string requested_by = 12; + string worker_id = 13; + uint64 fencing_token = 14; + google.protobuf.Timestamp lease_expires_at = 15; + google.protobuf.Timestamp last_heartbeat_at = 16; + google.protobuf.Timestamp available_at = 17; + google.protobuf.Timestamp started_at = 18; + google.protobuf.Timestamp finished_at = 19; + google.protobuf.Timestamp cancel_requested_at = 20; + int32 progress_percent = 21; + string current_step = 22; + string error_code = 23; + string error_message = 24; + google.protobuf.Timestamp created_at = 25; + google.protobuf.Timestamp updated_at = 26; + RepositoryDiagnosis diagnosis = 27; + string storage_node_id = 28; + string preflight_id = 29; + string commit_state = 30; + string commit_token = 31; + string prepared_revision_id = 32; + string prepared_manifest_sha256 = 33; + google.protobuf.Timestamp prepared_at = 34; + google.protobuf.Timestamp activated_at = 35; + google.protobuf.Timestamp confirmed_at = 36; + string result_object_id = 37; + string result_object_sha256 = 38; + string repository_name = 39; + string storage_node_name = 40; + string trigger = 41; + string idempotency_key = 42; + int64 bandwidth_limit_bytes = 43; + int32 max_parallel_downloads = 44; + string scope_type = 45; + string scope_id = 46; + string executor_kind = 47; + repeated RepositoryTaskTarget targets = 48; + string error_zh_message = 49; + string error_zh_recommended_action = 50; + repeated RepositoryTaskResultItem result_items = 51; +} + +message RepositoryTaskResultItem { + string id = 1; + string task_id = 2; + string item_key = 3; + string package_nevra = 4; + RepositoryTaskStatus status = 5; + string error_code = 6; + string message = 7; + string result_json = 8; + google.protobuf.Timestamp started_at = 9; + google.protobuf.Timestamp finished_at = 10; + google.protobuf.Timestamp created_at = 11; +} + +message RepositoryTaskTarget { + string id = 1; + string task_id = 2; + string target_type = 3; + string target_id = 4; + string storage_node_id = 5; + uint64 tms_agent_id = 6; + RepositoryTaskStatus status = 7; + int32 progress_percent = 8; + google.protobuf.Timestamp started_at = 9; + google.protobuf.Timestamp finished_at = 10; + string error_code = 11; + string error_message = 12; + string result_json = 13; + google.protobuf.Timestamp created_at = 14; + google.protobuf.Timestamp updated_at = 15; + string error_zh_message = 16; + string error_zh_recommended_action = 17; +} + +message DescribeRepositoryTaskRequest { + string task_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} + +message ListRepositoryTasksRequest { + string repository_id = 1 [(validate.rules).string = {max_len: 64}]; + RepositoryTaskStatus status = 2; + int64 project_id = 3 [(validate.rules).int64 = {gte: 0}]; + string storage_node_id = 4 [(validate.rules).string = {max_len: 64}]; + RepositoryTaskType type = 5; + string trigger = 6 [(validate.rules).string = {max_len: 32}]; + google.protobuf.Timestamp created_at_from = 7; + google.protobuf.Timestamp created_at_to = 8; + common.PageRequest page_request = 10; +} + +message RepositoryTaskStatistics { + int64 pending_count = 1; + int64 running_count = 2; + int64 failed_count = 3; + int64 success_count = 4; +} + +message CancelRepositoryTaskRequest { + string task_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} + +message RetryRepositoryTaskRequest { + string task_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} + +message RepositoryTaskResponse { + RepositoryTask task = 1; +} + +message RepositoryOCAIContext { + string task_id = 1; + string repository_id = 2; + string repository_name = 3; + string system_product = 4; + string product_version = 5; + string architecture = 6; + string task_type = 7; + string task_status = 8; + string storage_node_id = 9; + string revision_id = 10; + string current_step = 11; + string error_code = 12; + string error_message = 13; + RepositoryDiagnosis diagnosis = 14; + string log_summary = 15; + string context_json = 16; + string context_sha256 = 17; + bool truncated = 18; +} + +message RepositoryOCAIAnalysis { + string id = 1; + string task_id = 2; + string session_id = 3; + string context_sha256 = 4; + string status = 5; + string action_task_id = 6; + string action_result = 7; + string requested_by = 8; + google.protobuf.Timestamp created_at = 9; + google.protobuf.Timestamp feedback_at = 10; +} + +message BuildRepositoryOCAIContextRequest { + string task_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} + +message CreateRepositoryOCAIAnalysisRequest { + string task_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} + +message DescribeRepositoryOCAIAnalysisRequest { + string analysis_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} + +message AppendRepositoryOCAIFeedbackRequest { + string session_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + string action_task_id = 2 [(validate.rules).string = {min_len: 1, max_len: 64}]; + string action_result = 3 [(validate.rules).string = {min_len: 1, max_len: 32}]; +} + +message RepositoryOCAIContextResponse { + RepositoryOCAIContext context = 1; +} + +message RepositoryOCAIAnalysisResponse { + RepositoryOCAIAnalysis analysis = 1; + RepositoryOCAIContext context = 2; + string instruction = 3; +} + +message GenerateRepositoryConfigRequest { + string repository_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + bool immutable_revision = 2; +} + +message RepositoryConfig { + string repository_id = 1; + string active_revision_id = 2; + string repo_id = 3; + string file_name = 4; + string content_type = 5; + string content = 6; + string baseurl = 7; + string revision_url = 8; + bool enabled = 9; + bool gpgcheck = 10; + bool repo_gpgcheck = 11; + repeated string gpgkey_urls = 12; + bool immutable_revision = 13; +} + +message RepositoryConfigResponse { + RepositoryConfig config = 1; +} + +message RepositoryGPGKey { + string id = 1; + string repository_id = 2; + string key_id = 3; + string fingerprint = 4; + string public_key_path = 5; + string public_key_url = 6; + google.protobuf.Timestamp created_at = 7; +} + +message RegisterRepositoryGPGKeyRequest { + string repository_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + bytes public_key = 2; +} + +message ListRepositoryGPGKeysRequest { + string repository_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} + +message DeleteRepositoryGPGKeyRequest { + string repository_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + string key_id = 2 [(validate.rules).string = {min_len: 1, max_len: 64}]; + bool confirm_delete = 3; +} + +message RepositoryGPGKeyResponse { + RepositoryGPGKey key = 1; +} + +message ListRepositoryGPGKeysResponse { + repeated RepositoryGPGKey keys = 1; +} + +message ListRepositoryTasksResponse { + repeated RepositoryTask tasks = 1; + RepositoryTaskStatistics statistics = 2; + common.PageResponse page_response = 10; +} + +message RepositoryUpload { + string id = 1; + string repository_id = 2; + int64 project_id = 3; + string task_id = 4; + string file_name = 5; + string kind = 6; + int64 expected_size = 7; + int64 received_size = 8; + string expected_sha256 = 9; + string actual_sha256 = 10; + string status = 11; + string error_code = 12; + string error_message = 13; + google.protobuf.Timestamp expires_at = 14; + google.protobuf.Timestamp completed_at = 15; + google.protobuf.Timestamp created_at = 16; + google.protobuf.Timestamp updated_at = 17; + string storage_node_id = 18; + string client_fingerprint = 19; + int32 chunk_size = 20; + int32 total_chunks = 21; + int32 uploaded_count = 22; + google.protobuf.Timestamp paused_at = 23; +} + +message BeginRepositoryUploadRequest { + string repository_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + string file_name = 2 [(validate.rules).string = {min_len: 1, max_len: 255}]; + int64 total_size = 3 [(validate.rules).int64 = {gt: 0}]; + string expected_sha256 = 4 [(validate.rules).string = {max_len: 64}]; + string client_fingerprint = 5 [(validate.rules).string = {max_len: 64}]; + int32 chunk_size = 6 [(validate.rules).int32 = {gte: 0}]; +} + +message AppendRepositoryUploadChunkRequest { + string upload_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + int64 offset = 2 [(validate.rules).int64 = {gte: 0}]; + bytes data = 3; +} + +message PutRepositoryUploadChunkRequest { + string upload_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + int32 chunk_index = 2 [(validate.rules).int32 = {gte: 0}]; + string chunk_sha256 = 3 [(validate.rules).string = {len: 64}]; + bytes data = 4; +} + +message RepositoryUploadChunk { + int32 chunk_index = 1; + int32 size_bytes = 2; + string sha256 = 3; + google.protobuf.Timestamp created_at = 4; +} + +message PutRepositoryUploadChunkResponse { + RepositoryUpload upload = 1; + RepositoryUploadChunk chunk = 2; +} + +message DescribeRepositoryUploadChunksResponse { + RepositoryUpload upload = 1; + repeated RepositoryUploadChunk chunks = 2; + repeated int32 missing_chunk_indexes = 3; +} + +message CompleteRepositoryUploadRequest { + string upload_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} + +message AbortRepositoryUploadRequest { + string upload_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} + +message DescribeRepositoryUploadRequest { + string upload_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} + +message RepositoryUploadResponse { + RepositoryUpload upload = 1; +} + +message CompleteRepositoryUploadResponse { + RepositoryUpload upload = 1; + RepositoryTask task = 2; +} + +message RepositoryPackage { + string id = 1; + string repository_id = 2; + string revision_id = 3; + string name = 4; + string epoch = 5; + string version = 6; + string release = 7; + string architecture = 8; + string nevra = 9; + string file_name = 10; + int64 size_bytes = 11; + string sha256 = 12; + string source = 13; + google.protobuf.Timestamp created_at = 14; + string signature_status = 15; + string signer_key_id = 16; + string signature_message = 17; +} + +message ListRepositoryPackagesRequest { + string repository_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + string revision_id = 2 [(validate.rules).string = {max_len: 64}]; + string name = 3 [(validate.rules).string = {max_len: 255}]; + RepositoryArchitecture architecture = 4; + common.PageRequest page_request = 10; +} + +message ListRepositoryPackagesResponse { + string revision_id = 1; + repeated RepositoryPackage packages = 2; + common.PageResponse page_response = 10; +} + +message RepositoryQuarantineItem { + string id = 1; + string repository_id = 2; + string task_id = 3; + string upload_id = 4; + string original_name = 5; + int64 size_bytes = 6; + string sha256 = 7; + string reason_code = 8; + string reason_message = 9; + string detected_nevra = 10; + google.protobuf.Timestamp created_at = 11; +} + +message ListRepositoryQuarantineRequest { + string repository_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + string task_id = 2 [(validate.rules).string = {max_len: 64}]; + string reason_code = 3 [(validate.rules).string = {max_len: 64}]; + common.PageRequest page_request = 10; +} + +message ListRepositoryQuarantineResponse { + repeated RepositoryQuarantineItem items = 1; + common.PageResponse page_response = 10; +} + +message DeleteRepositoryPackageRequest { + string repository_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + string nevra = 2 [(validate.rules).string = {min_len: 1, max_len: 512}]; +} + +message RepositoryDiagnosis { + string failure_category = 1; + string error_code = 2; + string failed_step = 3; + string likely_cause = 4; + repeated string evidence = 5; + string recommended_action = 6; + string rule_id = 7; + google.protobuf.Timestamp generated_at = 8; +} + +message ExportRepositoryTaskDiagnosisRequest { + string task_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} + +message RepositoryDiagnosisReportResponse { + string file_name = 1; + string content_type = 2; + string sha256 = 3; + string content = 4; +} + +message RepositoryDistributionNode { + string agent_id = 1; + string ip = 2; + string asset_status = 3; + bool connected = 4; + google.protobuf.Timestamp last_heartbeat_at = 5; +} + +message ListRepositoryDistributionNodesRequest {} + +message ListRepositoryDistributionNodesResponse { + repeated RepositoryDistributionNode nodes = 1; +} + +message CreateRepositoryDistributionRequest { + string repository_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + string revision_id = 2 [(validate.rules).string = {max_len: 64}]; + repeated string target_ips = 3 [(validate.rules).repeated = {min_items: 1, max_items: 100, unique: true}]; + int32 timeout_seconds = 4 [(validate.rules).int32 = {gte: 0, lte: 1800}]; +} + +message DescribeRepositoryDistributionRequest { + string distribution_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} + +message ListRepositoryDistributionsRequest { + string repository_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + common.PageRequest page_request = 10; +} + +message RepositoryDistributionTarget { + string id = 1; + string target_ip = 2; + string agent_id = 3; + string status = 4; + int32 attempt_count = 5; + int32 exit_code = 6; + string output = 7; + string output_ref = 8; + bool output_truncated = 9; + string error_code = 10; + string error_message = 11; + google.protobuf.Timestamp started_at = 12; + google.protobuf.Timestamp finished_at = 13; +} + +message RepositoryDistribution { + string id = 1; + string repository_id = 2; + int64 project_id = 3; + string revision_id = 4; + string repository_task_id = 5; + string tms_task_id = 6; + string artifact_md5 = 7; + string manifest_sha256 = 8; + string repomd_sha256 = 9; + string status = 10; + string requested_by = 11; + int32 timeout_seconds = 12; + string error_code = 13; + string error_message = 14; + repeated RepositoryDistributionTarget targets = 15; + google.protobuf.Timestamp created_at = 16; + google.protobuf.Timestamp started_at = 17; + google.protobuf.Timestamp finished_at = 18; + google.protobuf.Timestamp updated_at = 19; +} + +message RepositoryDistributionResponse { + RepositoryDistribution distribution = 1; + RepositoryTask task = 2; +} + +message ListRepositoryDistributionsResponse { + repeated RepositoryDistribution distributions = 1; + common.PageResponse page_response = 10; +} + +message PreviewRepositoryPackageDeletionRequest { + string repository_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + repeated string nevras = 2 [(validate.rules).repeated = {min_items: 1, max_items: 100, unique: true}]; +} + +message ConfirmRepositoryPackageDeletionRequest { + string preview_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + string preview_digest = 2 [(validate.rules).string = {len: 64}]; +} + +message RepositoryPackageDeletionPreviewSource { + string repository_id = 1; + string revision_id = 2; + string repo_id = 3; + string baseurl = 4; +} + +message RepositoryPackageDependencyImpact { + string package_nevra = 1; + string requires = 2; + int32 depth = 3; + bool direct = 4; + string reason = 5; +} + +message RepositoryPackageDeletionPreview { + string preview_id = 1; + string preview_digest = 2; + string repository_id = 3; + string revision_id = 4; + repeated RepositoryPackageDeletionPreviewSource repository_set = 5; + repeated string nevras = 6; + repeated RepositoryPackageDependencyImpact dependency_impact = 7; + repeated string command = 8; + string raw_output = 9; + string status = 10; + google.protobuf.Timestamp expires_at = 11; + string created_by = 12; +} + +message RepositoryPackageDeletionPreviewResponse { + RepositoryPackageDeletionPreview preview = 1; +} + +message PreviewRepositoryPackageInstallRequest { + string repository_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + string revision_id = 2 [(validate.rules).string = {max_len: 64}]; + repeated string package_names = 3 [(validate.rules).repeated = {min_items: 1, max_items: 100, unique: true}]; + repeated string target_agent_ids = 4 [(validate.rules).repeated = {min_items: 1, max_items: 100, unique: true}]; + int32 timeout_seconds = 5 [(validate.rules).int32 = {gte: 0, lte: 1800}]; +} + +message RepositoryPackageInstallPreviewTarget { + string agent_id = 1; + string ip = 2; + RepositoryTaskStatus status = 3; + int32 exit_code = 4; + string error_code = 5; + string error_message = 6; + string raw_output = 7; + bool output_truncated = 8; + google.protobuf.Timestamp started_at = 9; + google.protobuf.Timestamp finished_at = 10; + repeated string selected_packages = 11; + repeated string dependency_packages = 12; + repeated string upgrade_packages = 13; + repeated string replacement_packages = 14; + int64 download_bytes = 15; + int64 disk_change_bytes = 16; +} + +message RepositoryPackageInstallPreview { + string preview_id = 1; + string preview_digest = 2; + string repository_id = 3; + string revision_id = 4; + string repo_id = 5; + repeated string package_names = 6; + repeated RepositoryPackageInstallPreviewTarget targets = 7; + string status = 8; + google.protobuf.Timestamp expires_at = 9; + string created_by = 10; +} + +message RepositoryPackageInstallPreviewResponse { + RepositoryPackageInstallPreview preview = 1; +} + +message ConfirmRepositoryPackageInstallRequest { + string preview_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + string preview_digest = 2 [(validate.rules).string = {len: 64}]; +} + +message DescribeRepositoryPackageInstallRequest { + string install_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} + +message RepositoryPackageInstall { + string id = 1; + int64 project_id = 2; + string repository_id = 3; + string revision_id = 4; + string preview_id = 5; + string repository_task_id = 6; + string tms_task_id = 7; + string status = 8; + int32 timeout_seconds = 9; + string requested_by = 10; + google.protobuf.Timestamp created_at = 11; + google.protobuf.Timestamp updated_at = 12; +} + +message RepositoryPackageInstallResponse { + RepositoryPackageInstall install = 1; + RepositoryTask task = 2; +} + +message ExportRepositoryInventoryRequest { + int64 project_id = 1 [(validate.rules).int64 = {gt: 0}]; +} + +message ExportRepositoryValidationReportRequest { + string repository_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} + +message ExportRepositoryDistributionResultRequest { + string distribution_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} + +message RepositoryExportResponse { + string file_name = 1; + string content_type = 2; + string sha256 = 3; + string content = 4; +} + +message RepositoryInventoryStorageNodeMapping { + string source_ref = 1 [(validate.rules).string = {min_len: 1, max_len: 128}]; + string target_storage_node_id = 2 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} + +message RepositoryInventorySigningKeyMapping { + string source_key_ref = 1 [(validate.rules).string = {min_len: 1, max_len: 128}]; + string target_key_ref = 2 [(validate.rules).string = {max_len: 128}]; +} + +message PreviewRepositoryInventoryImportRequest { + int64 project_id = 1 [(validate.rules).int64 = {gt: 0}]; + string content = 2 [(validate.rules).string = {min_len: 1, max_len: 10485760}]; + RepositoryInventoryConflictStrategy conflict_strategy = 3; + string default_storage_node_id = 4 [(validate.rules).string = {max_len: 64}]; + repeated RepositoryInventoryStorageNodeMapping storage_node_mappings = 5; + repeated RepositoryInventorySigningKeyMapping signing_key_mappings = 6; +} + +message RepositoryInventoryImportItem { + string source_repository_id = 1; + string source_name = 2; + string action = 3; + string message = 4; + RepositorySpec spec = 5; +} + +message RepositoryInventoryImportPreviewResponse { + string preview_id = 1; + string schema_version = 2; + string preview_digest = 3; + string resource_version = 4; + google.protobuf.Timestamp expires_at = 5; + string status = 6; + repeated RepositoryInventoryImportItem items = 7; + repeated string warnings = 8; +} + +message ConfirmRepositoryInventoryImportRequest { + string preview_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + string preview_digest = 2 [(validate.rules).string = {len: 64}]; +} + +message RepositoryInventoryImportJob { + string id = 1; + int64 project_id = 2; + string preview_id = 3; + string schema_version = 4; + string conflict_strategy = 5; + string status = 6; + string error_code = 7; + string error_message = 8; + string requested_by = 9; + google.protobuf.Timestamp created_at = 10; + google.protobuf.Timestamp finished_at = 11; + string repository_task_id = 12; +} + +message RepositoryInventoryImportResponse { + RepositoryInventoryImportJob job = 1; + repeated Repository repositories = 2; +} + +// Kept at the end so adding the migration API does not renumber existing +// generated message indexes. +message MigrateRepositoryRequest { + string repository_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + string target_storage_node_id = 2 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} + +message DescribeRepositoryTaskLogRequest { + string task_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} + +message DownloadRepositoryTaskLogRequest { + string task_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} + +message RepositoryTaskLog { + string task_id = 1; + string storage_node_id = 2; + string sha256 = 3; + int64 compressed_bytes = 4; + int64 uncompressed_bytes = 5; + int64 line_count = 6; + string summary = 7; + string error_fingerprint = 8; + google.protobuf.Timestamp expires_at = 9; + google.protobuf.Timestamp created_at = 10; +} + +message RepositoryTaskLogResponse { + RepositoryTaskLog task_log = 1; +} + +// Upstream profiles are reusable form templates. Repository creation resolves +// them into the existing RepositorySpec fields and does not persist a profile +// dependency in the repository runtime model. +message RepositoryUpstreamProfileSpec { + int64 project_id = 1 [(validate.rules).int64 = {gt: 0}]; + string name = 2 [(validate.rules).string = {min_len: 1, max_len: 128}]; + string description = 3 [(validate.rules).string = {max_len: 1024}]; + string system_product = 4 [(validate.rules).string = {min_len: 1, max_len: 128}]; + string channel = 5 [(validate.rules).string = {min_len: 1, max_len: 64}]; + string url_template = 6 [(validate.rules).string = {min_len: 1, max_len: 2048}]; + repeated string supported_versions = 7; + repeated RepositoryArchitecture supported_architectures = 8; + repeated string default_include_packages = 9; + repeated string default_exclude_packages = 10; + optional bool enabled = 11; +} + +message RepositoryUpstreamProfile { + string id = 1; + int64 project_id = 2; + string name = 3; + string description = 4; + string system_product = 5; + string channel = 6; + string url_template = 7; + repeated string supported_versions = 8; + repeated RepositoryArchitecture supported_architectures = 9; + repeated string default_include_packages = 10; + repeated string default_exclude_packages = 11; + bool enabled = 12; + string created_by = 13; + google.protobuf.Timestamp created_at = 14; + google.protobuf.Timestamp updated_at = 15; +} + +message CreateRepositoryUpstreamProfileRequest { + RepositoryUpstreamProfileSpec spec = 1; +} + +message UpdateRepositoryUpstreamProfileRequest { + string upstream_profile_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + RepositoryUpstreamProfileSpec spec = 2; +} + +message DescribeRepositoryUpstreamProfileRequest { + string upstream_profile_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} + +message ListRepositoryUpstreamProfilesRequest { + int64 project_id = 1 [(validate.rules).int64 = {gt: 0}]; + string system_product = 2 [(validate.rules).string = {max_len: 128}]; + string keyword = 3 [(validate.rules).string = {max_len: 128}]; + optional bool enabled = 4; + common.PageRequest page_request = 10; +} + +message DeleteRepositoryUpstreamProfileRequest { + string upstream_profile_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} + +message RepositoryUpstreamProfileResponse { + RepositoryUpstreamProfile profile = 1; +} + +message ListRepositoryUpstreamProfilesResponse { + repeated RepositoryUpstreamProfile profiles = 1; + common.PageResponse page_response = 10; +} diff --git a/manager/backend/protocol/repository/repository.trpc.go b/manager/backend/protocol/repository/repository.trpc.go new file mode 100644 index 0000000000000000000000000000000000000000..b51a1db82492a45d0e9c1963224fd843704af559 --- /dev/null +++ b/manager/backend/protocol/repository/repository.trpc.go @@ -0,0 +1,3639 @@ +// Code generated by trpc-go/trpc-cmdline v1.0.9. DO NOT EDIT. +// source: repository/repository.proto + +package repository + +import ( + "context" + "errors" + "fmt" + + emptypb "google.golang.org/protobuf/types/known/emptypb" + _ "trpc.group/trpc-go/trpc-go" + "trpc.group/trpc-go/trpc-go/client" + "trpc.group/trpc-go/trpc-go/codec" + _ "trpc.group/trpc-go/trpc-go/http" + "trpc.group/trpc-go/trpc-go/server" +) + +// START ======================================= Server Service Definition ======================================= START + +// RepositoryServiceService defines service. +type RepositoryServiceService interface { + CreateRepository(ctx context.Context, req *CreateRepositoryRequest) (*RepositoryResponse, error) + + UpdateRepository(ctx context.Context, req *UpdateRepositoryRequest) (*RepositoryResponse, error) + + DescribeRepository(ctx context.Context, req *DescribeRepositoryRequest) (*RepositoryResponse, error) + + ListRepositories(ctx context.Context, req *ListRepositoriesRequest) (*ListRepositoriesResponse, error) + + DeleteRepository(ctx context.Context, req *DeleteRepositoryRequest) (*emptypb.Empty, error) + + BatchApplyRepositories(ctx context.Context, req *BatchApplyRepositoriesRequest) (*BatchApplyRepositoriesResponse, error) + + CreateRepositoryUpstreamProfile(ctx context.Context, req *CreateRepositoryUpstreamProfileRequest) (*RepositoryUpstreamProfileResponse, error) + + UpdateRepositoryUpstreamProfile(ctx context.Context, req *UpdateRepositoryUpstreamProfileRequest) (*RepositoryUpstreamProfileResponse, error) + + DescribeRepositoryUpstreamProfile(ctx context.Context, req *DescribeRepositoryUpstreamProfileRequest) (*RepositoryUpstreamProfileResponse, error) + + ListRepositoryUpstreamProfiles(ctx context.Context, req *ListRepositoryUpstreamProfilesRequest) (*ListRepositoryUpstreamProfilesResponse, error) + + DeleteRepositoryUpstreamProfile(ctx context.Context, req *DeleteRepositoryUpstreamProfileRequest) (*emptypb.Empty, error) + + CreateRepositoryStorageNode(ctx context.Context, req *CreateRepositoryStorageNodeRequest) (*RepositoryStorageNodeResponse, error) + + UpdateRepositoryStorageNode(ctx context.Context, req *UpdateRepositoryStorageNodeRequest) (*RepositoryStorageNodeResponse, error) + + DescribeRepositoryStorageNode(ctx context.Context, req *DescribeRepositoryStorageNodeRequest) (*RepositoryStorageNodeResponse, error) + + ListRepositoryStorageNodes(ctx context.Context, req *ListRepositoryStorageNodesRequest) (*ListRepositoryStorageNodesResponse, error) + + DeleteRepositoryStorageNode(ctx context.Context, req *DeleteRepositoryStorageNodeRequest) (*emptypb.Empty, error) + + ProbeRepositoryStorageNode(ctx context.Context, req *ProbeRepositoryStorageNodeRequest) (*RepositoryStorageNodeProbeResponse, error) + + ListRepositoryPreflightRecords(ctx context.Context, req *ListRepositoryPreflightRecordsRequest) (*ListRepositoryPreflightRecordsResponse, error) + + CreateRepositoryNodeEnrollment(ctx context.Context, req *CreateRepositoryNodeEnrollmentRequest) (*RepositoryNodeEnrollmentResponse, error) + + RegisterRepositoryNodeArtifact(ctx context.Context, req *RegisterRepositoryNodeArtifactRequest) (*RepositoryNodeArtifactResponse, error) + + ListRepositoryNodeArtifacts(ctx context.Context, req *ListRepositoryNodeArtifactsRequest) (*ListRepositoryNodeArtifactsResponse, error) + + DownloadRepositoryNodeArtifact(ctx context.Context, req *DownloadRepositoryNodeArtifactRequest) (*emptypb.Empty, error) + + ConfigureRepositoryNodeInstallSource(ctx context.Context, req *ConfigureRepositoryNodeInstallSourceRequest) (*RepositoryNodeInstallSourceResponse, error) + + ListRepositoryNodeInstallSources(ctx context.Context, req *ListRepositoryNodeInstallSourcesRequest) (*ListRepositoryNodeInstallSourcesResponse, error) + + UpsertRepositoryNodeRegistrationAllowlist(ctx context.Context, req *UpsertRepositoryNodeRegistrationAllowlistRequest) (*RepositoryNodeRegistrationAllowlistResponse, error) + + ListRepositoryNodeRegistrationAllowlists(ctx context.Context, req *ListRepositoryNodeRegistrationAllowlistsRequest) (*ListRepositoryNodeRegistrationAllowlistsResponse, error) + + SyncRepository(ctx context.Context, req *SyncRepositoryRequest) (*RepositoryTaskResponse, error) + + UpsertRepositorySyncSchedule(ctx context.Context, req *UpsertRepositorySyncScheduleRequest) (*RepositorySyncScheduleResponse, error) + + DescribeRepositorySyncSchedule(ctx context.Context, req *DescribeRepositorySyncScheduleRequest) (*RepositorySyncScheduleResponse, error) + + PauseRepositorySyncSchedule(ctx context.Context, req *ChangeRepositorySyncScheduleStatusRequest) (*RepositorySyncScheduleResponse, error) + + ResumeRepositorySyncSchedule(ctx context.Context, req *ChangeRepositorySyncScheduleStatusRequest) (*RepositorySyncScheduleResponse, error) + + GenerateRepositoryConfig(ctx context.Context, req *GenerateRepositoryConfigRequest) (*RepositoryConfigResponse, error) + + RegisterRepositoryGPGKey(ctx context.Context, req *RegisterRepositoryGPGKeyRequest) (*RepositoryGPGKeyResponse, error) + + ListRepositoryGPGKeys(ctx context.Context, req *ListRepositoryGPGKeysRequest) (*ListRepositoryGPGKeysResponse, error) + + DeleteRepositoryGPGKey(ctx context.Context, req *DeleteRepositoryGPGKeyRequest) (*emptypb.Empty, error) + + DescribeRepositoryTask(ctx context.Context, req *DescribeRepositoryTaskRequest) (*RepositoryTaskResponse, error) + + ListRepositoryTasks(ctx context.Context, req *ListRepositoryTasksRequest) (*ListRepositoryTasksResponse, error) + + CancelRepositoryTask(ctx context.Context, req *CancelRepositoryTaskRequest) (*RepositoryTaskResponse, error) + + RetryRepositoryTask(ctx context.Context, req *RetryRepositoryTaskRequest) (*RepositoryTaskResponse, error) + + ExportRepositoryTaskDiagnosis(ctx context.Context, req *ExportRepositoryTaskDiagnosisRequest) (*RepositoryDiagnosisReportResponse, error) + + DescribeRepositoryTaskLog(ctx context.Context, req *DescribeRepositoryTaskLogRequest) (*RepositoryTaskLogResponse, error) + + DownloadRepositoryTaskLog(ctx context.Context, req *DownloadRepositoryTaskLogRequest) (*emptypb.Empty, error) + + BuildRepositoryOCAIContext(ctx context.Context, req *BuildRepositoryOCAIContextRequest) (*RepositoryOCAIContextResponse, error) + + CreateRepositoryOCAIAnalysis(ctx context.Context, req *CreateRepositoryOCAIAnalysisRequest) (*RepositoryOCAIAnalysisResponse, error) + + DescribeRepositoryOCAIAnalysis(ctx context.Context, req *DescribeRepositoryOCAIAnalysisRequest) (*RepositoryOCAIAnalysisResponse, error) + + AppendRepositoryOCAIFeedback(ctx context.Context, req *AppendRepositoryOCAIFeedbackRequest) (*RepositoryOCAIAnalysisResponse, error) + + BeginRepositoryUpload(ctx context.Context, req *BeginRepositoryUploadRequest) (*RepositoryUploadResponse, error) + + AppendRepositoryUploadChunk(ctx context.Context, req *AppendRepositoryUploadChunkRequest) (*RepositoryUploadResponse, error) + + PutRepositoryUploadChunk(ctx context.Context, req *PutRepositoryUploadChunkRequest) (*PutRepositoryUploadChunkResponse, error) + + DescribeRepositoryUploadChunks(ctx context.Context, req *DescribeRepositoryUploadRequest) (*DescribeRepositoryUploadChunksResponse, error) + + PauseRepositoryUpload(ctx context.Context, req *DescribeRepositoryUploadRequest) (*RepositoryUploadResponse, error) + + ResumeRepositoryUpload(ctx context.Context, req *DescribeRepositoryUploadRequest) (*RepositoryUploadResponse, error) + + CompleteRepositoryUpload(ctx context.Context, req *CompleteRepositoryUploadRequest) (*CompleteRepositoryUploadResponse, error) + + AbortRepositoryUpload(ctx context.Context, req *AbortRepositoryUploadRequest) (*RepositoryUploadResponse, error) + + DescribeRepositoryUpload(ctx context.Context, req *DescribeRepositoryUploadRequest) (*RepositoryUploadResponse, error) + + ListRepositoryPackages(ctx context.Context, req *ListRepositoryPackagesRequest) (*ListRepositoryPackagesResponse, error) + + ListRepositoryQuarantine(ctx context.Context, req *ListRepositoryQuarantineRequest) (*ListRepositoryQuarantineResponse, error) + + DeleteRepositoryPackage(ctx context.Context, req *DeleteRepositoryPackageRequest) (*RepositoryTaskResponse, error) + + PreviewRepositoryPackageDeletion(ctx context.Context, req *PreviewRepositoryPackageDeletionRequest) (*RepositoryPackageDeletionPreviewResponse, error) + + ConfirmRepositoryPackageDeletion(ctx context.Context, req *ConfirmRepositoryPackageDeletionRequest) (*RepositoryTaskResponse, error) + + ListRepositoryDistributionNodes(ctx context.Context, req *ListRepositoryDistributionNodesRequest) (*ListRepositoryDistributionNodesResponse, error) + + CreateRepositoryDistribution(ctx context.Context, req *CreateRepositoryDistributionRequest) (*RepositoryDistributionResponse, error) + + DescribeRepositoryDistribution(ctx context.Context, req *DescribeRepositoryDistributionRequest) (*RepositoryDistributionResponse, error) + + ListRepositoryDistributions(ctx context.Context, req *ListRepositoryDistributionsRequest) (*ListRepositoryDistributionsResponse, error) + + PreviewRepositoryPackageInstall(ctx context.Context, req *PreviewRepositoryPackageInstallRequest) (*RepositoryPackageInstallPreviewResponse, error) + + ConfirmRepositoryPackageInstall(ctx context.Context, req *ConfirmRepositoryPackageInstallRequest) (*RepositoryPackageInstallResponse, error) + + DescribeRepositoryPackageInstall(ctx context.Context, req *DescribeRepositoryPackageInstallRequest) (*RepositoryPackageInstallResponse, error) + + ExportRepositoryInventory(ctx context.Context, req *ExportRepositoryInventoryRequest) (*RepositoryExportResponse, error) + + PreviewRepositoryInventoryImport(ctx context.Context, req *PreviewRepositoryInventoryImportRequest) (*RepositoryInventoryImportPreviewResponse, error) + + ConfirmRepositoryInventoryImport(ctx context.Context, req *ConfirmRepositoryInventoryImportRequest) (*RepositoryInventoryImportResponse, error) + + ExportRepositoryValidationReport(ctx context.Context, req *ExportRepositoryValidationReportRequest) (*RepositoryExportResponse, error) + + ExportRepositoryDistributionResult(ctx context.Context, req *ExportRepositoryDistributionResultRequest) (*RepositoryExportResponse, error) + + MigrateRepository(ctx context.Context, req *MigrateRepositoryRequest) (*RepositoryTaskResponse, error) +} + +func RepositoryServiceService_CreateRepository_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &CreateRepositoryRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).CreateRepository(ctx, reqbody.(*CreateRepositoryRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_UpdateRepository_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &UpdateRepositoryRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).UpdateRepository(ctx, reqbody.(*UpdateRepositoryRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_DescribeRepository_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &DescribeRepositoryRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).DescribeRepository(ctx, reqbody.(*DescribeRepositoryRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_ListRepositories_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &ListRepositoriesRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).ListRepositories(ctx, reqbody.(*ListRepositoriesRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_DeleteRepository_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &DeleteRepositoryRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).DeleteRepository(ctx, reqbody.(*DeleteRepositoryRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_BatchApplyRepositories_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &BatchApplyRepositoriesRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).BatchApplyRepositories(ctx, reqbody.(*BatchApplyRepositoriesRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_CreateRepositoryUpstreamProfile_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &CreateRepositoryUpstreamProfileRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).CreateRepositoryUpstreamProfile(ctx, reqbody.(*CreateRepositoryUpstreamProfileRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_UpdateRepositoryUpstreamProfile_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &UpdateRepositoryUpstreamProfileRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).UpdateRepositoryUpstreamProfile(ctx, reqbody.(*UpdateRepositoryUpstreamProfileRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_DescribeRepositoryUpstreamProfile_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &DescribeRepositoryUpstreamProfileRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).DescribeRepositoryUpstreamProfile(ctx, reqbody.(*DescribeRepositoryUpstreamProfileRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_ListRepositoryUpstreamProfiles_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &ListRepositoryUpstreamProfilesRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).ListRepositoryUpstreamProfiles(ctx, reqbody.(*ListRepositoryUpstreamProfilesRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_DeleteRepositoryUpstreamProfile_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &DeleteRepositoryUpstreamProfileRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).DeleteRepositoryUpstreamProfile(ctx, reqbody.(*DeleteRepositoryUpstreamProfileRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_CreateRepositoryStorageNode_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &CreateRepositoryStorageNodeRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).CreateRepositoryStorageNode(ctx, reqbody.(*CreateRepositoryStorageNodeRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_UpdateRepositoryStorageNode_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &UpdateRepositoryStorageNodeRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).UpdateRepositoryStorageNode(ctx, reqbody.(*UpdateRepositoryStorageNodeRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_DescribeRepositoryStorageNode_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &DescribeRepositoryStorageNodeRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).DescribeRepositoryStorageNode(ctx, reqbody.(*DescribeRepositoryStorageNodeRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_ListRepositoryStorageNodes_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &ListRepositoryStorageNodesRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).ListRepositoryStorageNodes(ctx, reqbody.(*ListRepositoryStorageNodesRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_DeleteRepositoryStorageNode_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &DeleteRepositoryStorageNodeRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).DeleteRepositoryStorageNode(ctx, reqbody.(*DeleteRepositoryStorageNodeRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_ProbeRepositoryStorageNode_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &ProbeRepositoryStorageNodeRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).ProbeRepositoryStorageNode(ctx, reqbody.(*ProbeRepositoryStorageNodeRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_ListRepositoryPreflightRecords_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &ListRepositoryPreflightRecordsRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).ListRepositoryPreflightRecords(ctx, reqbody.(*ListRepositoryPreflightRecordsRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_CreateRepositoryNodeEnrollment_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &CreateRepositoryNodeEnrollmentRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).CreateRepositoryNodeEnrollment(ctx, reqbody.(*CreateRepositoryNodeEnrollmentRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_RegisterRepositoryNodeArtifact_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &RegisterRepositoryNodeArtifactRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).RegisterRepositoryNodeArtifact(ctx, reqbody.(*RegisterRepositoryNodeArtifactRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_ListRepositoryNodeArtifacts_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &ListRepositoryNodeArtifactsRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).ListRepositoryNodeArtifacts(ctx, reqbody.(*ListRepositoryNodeArtifactsRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_DownloadRepositoryNodeArtifact_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &DownloadRepositoryNodeArtifactRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).DownloadRepositoryNodeArtifact(ctx, reqbody.(*DownloadRepositoryNodeArtifactRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_ConfigureRepositoryNodeInstallSource_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &ConfigureRepositoryNodeInstallSourceRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).ConfigureRepositoryNodeInstallSource(ctx, reqbody.(*ConfigureRepositoryNodeInstallSourceRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_ListRepositoryNodeInstallSources_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &ListRepositoryNodeInstallSourcesRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).ListRepositoryNodeInstallSources(ctx, reqbody.(*ListRepositoryNodeInstallSourcesRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_UpsertRepositoryNodeRegistrationAllowlist_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &UpsertRepositoryNodeRegistrationAllowlistRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).UpsertRepositoryNodeRegistrationAllowlist(ctx, reqbody.(*UpsertRepositoryNodeRegistrationAllowlistRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_ListRepositoryNodeRegistrationAllowlists_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &ListRepositoryNodeRegistrationAllowlistsRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).ListRepositoryNodeRegistrationAllowlists(ctx, reqbody.(*ListRepositoryNodeRegistrationAllowlistsRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_SyncRepository_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &SyncRepositoryRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).SyncRepository(ctx, reqbody.(*SyncRepositoryRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_UpsertRepositorySyncSchedule_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &UpsertRepositorySyncScheduleRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).UpsertRepositorySyncSchedule(ctx, reqbody.(*UpsertRepositorySyncScheduleRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_DescribeRepositorySyncSchedule_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &DescribeRepositorySyncScheduleRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).DescribeRepositorySyncSchedule(ctx, reqbody.(*DescribeRepositorySyncScheduleRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_PauseRepositorySyncSchedule_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &ChangeRepositorySyncScheduleStatusRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).PauseRepositorySyncSchedule(ctx, reqbody.(*ChangeRepositorySyncScheduleStatusRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_ResumeRepositorySyncSchedule_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &ChangeRepositorySyncScheduleStatusRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).ResumeRepositorySyncSchedule(ctx, reqbody.(*ChangeRepositorySyncScheduleStatusRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_GenerateRepositoryConfig_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &GenerateRepositoryConfigRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).GenerateRepositoryConfig(ctx, reqbody.(*GenerateRepositoryConfigRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_RegisterRepositoryGPGKey_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &RegisterRepositoryGPGKeyRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).RegisterRepositoryGPGKey(ctx, reqbody.(*RegisterRepositoryGPGKeyRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_ListRepositoryGPGKeys_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &ListRepositoryGPGKeysRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).ListRepositoryGPGKeys(ctx, reqbody.(*ListRepositoryGPGKeysRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_DeleteRepositoryGPGKey_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &DeleteRepositoryGPGKeyRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).DeleteRepositoryGPGKey(ctx, reqbody.(*DeleteRepositoryGPGKeyRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_DescribeRepositoryTask_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &DescribeRepositoryTaskRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).DescribeRepositoryTask(ctx, reqbody.(*DescribeRepositoryTaskRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_ListRepositoryTasks_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &ListRepositoryTasksRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).ListRepositoryTasks(ctx, reqbody.(*ListRepositoryTasksRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_CancelRepositoryTask_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &CancelRepositoryTaskRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).CancelRepositoryTask(ctx, reqbody.(*CancelRepositoryTaskRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_RetryRepositoryTask_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &RetryRepositoryTaskRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).RetryRepositoryTask(ctx, reqbody.(*RetryRepositoryTaskRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_ExportRepositoryTaskDiagnosis_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &ExportRepositoryTaskDiagnosisRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).ExportRepositoryTaskDiagnosis(ctx, reqbody.(*ExportRepositoryTaskDiagnosisRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_DescribeRepositoryTaskLog_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &DescribeRepositoryTaskLogRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).DescribeRepositoryTaskLog(ctx, reqbody.(*DescribeRepositoryTaskLogRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_DownloadRepositoryTaskLog_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &DownloadRepositoryTaskLogRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).DownloadRepositoryTaskLog(ctx, reqbody.(*DownloadRepositoryTaskLogRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_BuildRepositoryOCAIContext_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &BuildRepositoryOCAIContextRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).BuildRepositoryOCAIContext(ctx, reqbody.(*BuildRepositoryOCAIContextRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_CreateRepositoryOCAIAnalysis_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &CreateRepositoryOCAIAnalysisRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).CreateRepositoryOCAIAnalysis(ctx, reqbody.(*CreateRepositoryOCAIAnalysisRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_DescribeRepositoryOCAIAnalysis_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &DescribeRepositoryOCAIAnalysisRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).DescribeRepositoryOCAIAnalysis(ctx, reqbody.(*DescribeRepositoryOCAIAnalysisRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_AppendRepositoryOCAIFeedback_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &AppendRepositoryOCAIFeedbackRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).AppendRepositoryOCAIFeedback(ctx, reqbody.(*AppendRepositoryOCAIFeedbackRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_BeginRepositoryUpload_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &BeginRepositoryUploadRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).BeginRepositoryUpload(ctx, reqbody.(*BeginRepositoryUploadRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_AppendRepositoryUploadChunk_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &AppendRepositoryUploadChunkRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).AppendRepositoryUploadChunk(ctx, reqbody.(*AppendRepositoryUploadChunkRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_PutRepositoryUploadChunk_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &PutRepositoryUploadChunkRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).PutRepositoryUploadChunk(ctx, reqbody.(*PutRepositoryUploadChunkRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_DescribeRepositoryUploadChunks_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &DescribeRepositoryUploadRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).DescribeRepositoryUploadChunks(ctx, reqbody.(*DescribeRepositoryUploadRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_PauseRepositoryUpload_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &DescribeRepositoryUploadRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).PauseRepositoryUpload(ctx, reqbody.(*DescribeRepositoryUploadRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_ResumeRepositoryUpload_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &DescribeRepositoryUploadRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).ResumeRepositoryUpload(ctx, reqbody.(*DescribeRepositoryUploadRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_CompleteRepositoryUpload_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &CompleteRepositoryUploadRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).CompleteRepositoryUpload(ctx, reqbody.(*CompleteRepositoryUploadRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_AbortRepositoryUpload_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &AbortRepositoryUploadRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).AbortRepositoryUpload(ctx, reqbody.(*AbortRepositoryUploadRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_DescribeRepositoryUpload_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &DescribeRepositoryUploadRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).DescribeRepositoryUpload(ctx, reqbody.(*DescribeRepositoryUploadRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_ListRepositoryPackages_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &ListRepositoryPackagesRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).ListRepositoryPackages(ctx, reqbody.(*ListRepositoryPackagesRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_ListRepositoryQuarantine_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &ListRepositoryQuarantineRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).ListRepositoryQuarantine(ctx, reqbody.(*ListRepositoryQuarantineRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_DeleteRepositoryPackage_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &DeleteRepositoryPackageRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).DeleteRepositoryPackage(ctx, reqbody.(*DeleteRepositoryPackageRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_PreviewRepositoryPackageDeletion_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &PreviewRepositoryPackageDeletionRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).PreviewRepositoryPackageDeletion(ctx, reqbody.(*PreviewRepositoryPackageDeletionRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_ConfirmRepositoryPackageDeletion_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &ConfirmRepositoryPackageDeletionRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).ConfirmRepositoryPackageDeletion(ctx, reqbody.(*ConfirmRepositoryPackageDeletionRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_ListRepositoryDistributionNodes_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &ListRepositoryDistributionNodesRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).ListRepositoryDistributionNodes(ctx, reqbody.(*ListRepositoryDistributionNodesRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_CreateRepositoryDistribution_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &CreateRepositoryDistributionRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).CreateRepositoryDistribution(ctx, reqbody.(*CreateRepositoryDistributionRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_DescribeRepositoryDistribution_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &DescribeRepositoryDistributionRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).DescribeRepositoryDistribution(ctx, reqbody.(*DescribeRepositoryDistributionRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_ListRepositoryDistributions_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &ListRepositoryDistributionsRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).ListRepositoryDistributions(ctx, reqbody.(*ListRepositoryDistributionsRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_PreviewRepositoryPackageInstall_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &PreviewRepositoryPackageInstallRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).PreviewRepositoryPackageInstall(ctx, reqbody.(*PreviewRepositoryPackageInstallRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_ConfirmRepositoryPackageInstall_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &ConfirmRepositoryPackageInstallRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).ConfirmRepositoryPackageInstall(ctx, reqbody.(*ConfirmRepositoryPackageInstallRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_DescribeRepositoryPackageInstall_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &DescribeRepositoryPackageInstallRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).DescribeRepositoryPackageInstall(ctx, reqbody.(*DescribeRepositoryPackageInstallRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_ExportRepositoryInventory_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &ExportRepositoryInventoryRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).ExportRepositoryInventory(ctx, reqbody.(*ExportRepositoryInventoryRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_PreviewRepositoryInventoryImport_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &PreviewRepositoryInventoryImportRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).PreviewRepositoryInventoryImport(ctx, reqbody.(*PreviewRepositoryInventoryImportRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_ConfirmRepositoryInventoryImport_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &ConfirmRepositoryInventoryImportRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).ConfirmRepositoryInventoryImport(ctx, reqbody.(*ConfirmRepositoryInventoryImportRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_ExportRepositoryValidationReport_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &ExportRepositoryValidationReportRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).ExportRepositoryValidationReport(ctx, reqbody.(*ExportRepositoryValidationReportRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_ExportRepositoryDistributionResult_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &ExportRepositoryDistributionResultRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).ExportRepositoryDistributionResult(ctx, reqbody.(*ExportRepositoryDistributionResultRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryServiceService_MigrateRepository_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &MigrateRepositoryRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryServiceService).MigrateRepository(ctx, reqbody.(*MigrateRepositoryRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +// RepositoryServiceServer_ServiceDesc descriptor for server.RegisterService. +var RepositoryServiceServer_ServiceDesc = server.ServiceDesc{ + ServiceName: "trpc.ocmanager.repository.RepositoryService", + HandlerType: ((*RepositoryServiceService)(nil)), + Methods: []server.Method{ + { + Name: "/trpc.ocmanager.repository.RepositoryService/CreateRepository", + Func: RepositoryServiceService_CreateRepository_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/UpdateRepository", + Func: RepositoryServiceService_UpdateRepository_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/DescribeRepository", + Func: RepositoryServiceService_DescribeRepository_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/ListRepositories", + Func: RepositoryServiceService_ListRepositories_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/DeleteRepository", + Func: RepositoryServiceService_DeleteRepository_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/BatchApplyRepositories", + Func: RepositoryServiceService_BatchApplyRepositories_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/CreateRepositoryUpstreamProfile", + Func: RepositoryServiceService_CreateRepositoryUpstreamProfile_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/UpdateRepositoryUpstreamProfile", + Func: RepositoryServiceService_UpdateRepositoryUpstreamProfile_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/DescribeRepositoryUpstreamProfile", + Func: RepositoryServiceService_DescribeRepositoryUpstreamProfile_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/ListRepositoryUpstreamProfiles", + Func: RepositoryServiceService_ListRepositoryUpstreamProfiles_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/DeleteRepositoryUpstreamProfile", + Func: RepositoryServiceService_DeleteRepositoryUpstreamProfile_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/CreateRepositoryStorageNode", + Func: RepositoryServiceService_CreateRepositoryStorageNode_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/UpdateRepositoryStorageNode", + Func: RepositoryServiceService_UpdateRepositoryStorageNode_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/DescribeRepositoryStorageNode", + Func: RepositoryServiceService_DescribeRepositoryStorageNode_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/ListRepositoryStorageNodes", + Func: RepositoryServiceService_ListRepositoryStorageNodes_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/DeleteRepositoryStorageNode", + Func: RepositoryServiceService_DeleteRepositoryStorageNode_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/ProbeRepositoryStorageNode", + Func: RepositoryServiceService_ProbeRepositoryStorageNode_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/ListRepositoryPreflightRecords", + Func: RepositoryServiceService_ListRepositoryPreflightRecords_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/CreateRepositoryNodeEnrollment", + Func: RepositoryServiceService_CreateRepositoryNodeEnrollment_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/RegisterRepositoryNodeArtifact", + Func: RepositoryServiceService_RegisterRepositoryNodeArtifact_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/ListRepositoryNodeArtifacts", + Func: RepositoryServiceService_ListRepositoryNodeArtifacts_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/DownloadRepositoryNodeArtifact", + Func: RepositoryServiceService_DownloadRepositoryNodeArtifact_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/ConfigureRepositoryNodeInstallSource", + Func: RepositoryServiceService_ConfigureRepositoryNodeInstallSource_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/ListRepositoryNodeInstallSources", + Func: RepositoryServiceService_ListRepositoryNodeInstallSources_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/UpsertRepositoryNodeRegistrationAllowlist", + Func: RepositoryServiceService_UpsertRepositoryNodeRegistrationAllowlist_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/ListRepositoryNodeRegistrationAllowlists", + Func: RepositoryServiceService_ListRepositoryNodeRegistrationAllowlists_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/SyncRepository", + Func: RepositoryServiceService_SyncRepository_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/UpsertRepositorySyncSchedule", + Func: RepositoryServiceService_UpsertRepositorySyncSchedule_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/DescribeRepositorySyncSchedule", + Func: RepositoryServiceService_DescribeRepositorySyncSchedule_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/PauseRepositorySyncSchedule", + Func: RepositoryServiceService_PauseRepositorySyncSchedule_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/ResumeRepositorySyncSchedule", + Func: RepositoryServiceService_ResumeRepositorySyncSchedule_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/GenerateRepositoryConfig", + Func: RepositoryServiceService_GenerateRepositoryConfig_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/RegisterRepositoryGPGKey", + Func: RepositoryServiceService_RegisterRepositoryGPGKey_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/ListRepositoryGPGKeys", + Func: RepositoryServiceService_ListRepositoryGPGKeys_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/DeleteRepositoryGPGKey", + Func: RepositoryServiceService_DeleteRepositoryGPGKey_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/DescribeRepositoryTask", + Func: RepositoryServiceService_DescribeRepositoryTask_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/ListRepositoryTasks", + Func: RepositoryServiceService_ListRepositoryTasks_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/CancelRepositoryTask", + Func: RepositoryServiceService_CancelRepositoryTask_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/RetryRepositoryTask", + Func: RepositoryServiceService_RetryRepositoryTask_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/ExportRepositoryTaskDiagnosis", + Func: RepositoryServiceService_ExportRepositoryTaskDiagnosis_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/DescribeRepositoryTaskLog", + Func: RepositoryServiceService_DescribeRepositoryTaskLog_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/DownloadRepositoryTaskLog", + Func: RepositoryServiceService_DownloadRepositoryTaskLog_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/BuildRepositoryOCAIContext", + Func: RepositoryServiceService_BuildRepositoryOCAIContext_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/CreateRepositoryOCAIAnalysis", + Func: RepositoryServiceService_CreateRepositoryOCAIAnalysis_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/DescribeRepositoryOCAIAnalysis", + Func: RepositoryServiceService_DescribeRepositoryOCAIAnalysis_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/AppendRepositoryOCAIFeedback", + Func: RepositoryServiceService_AppendRepositoryOCAIFeedback_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/BeginRepositoryUpload", + Func: RepositoryServiceService_BeginRepositoryUpload_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/AppendRepositoryUploadChunk", + Func: RepositoryServiceService_AppendRepositoryUploadChunk_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/PutRepositoryUploadChunk", + Func: RepositoryServiceService_PutRepositoryUploadChunk_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/DescribeRepositoryUploadChunks", + Func: RepositoryServiceService_DescribeRepositoryUploadChunks_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/PauseRepositoryUpload", + Func: RepositoryServiceService_PauseRepositoryUpload_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/ResumeRepositoryUpload", + Func: RepositoryServiceService_ResumeRepositoryUpload_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/CompleteRepositoryUpload", + Func: RepositoryServiceService_CompleteRepositoryUpload_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/AbortRepositoryUpload", + Func: RepositoryServiceService_AbortRepositoryUpload_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/DescribeRepositoryUpload", + Func: RepositoryServiceService_DescribeRepositoryUpload_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/ListRepositoryPackages", + Func: RepositoryServiceService_ListRepositoryPackages_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/ListRepositoryQuarantine", + Func: RepositoryServiceService_ListRepositoryQuarantine_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/DeleteRepositoryPackage", + Func: RepositoryServiceService_DeleteRepositoryPackage_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/PreviewRepositoryPackageDeletion", + Func: RepositoryServiceService_PreviewRepositoryPackageDeletion_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/ConfirmRepositoryPackageDeletion", + Func: RepositoryServiceService_ConfirmRepositoryPackageDeletion_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/ListRepositoryDistributionNodes", + Func: RepositoryServiceService_ListRepositoryDistributionNodes_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/CreateRepositoryDistribution", + Func: RepositoryServiceService_CreateRepositoryDistribution_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/DescribeRepositoryDistribution", + Func: RepositoryServiceService_DescribeRepositoryDistribution_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/ListRepositoryDistributions", + Func: RepositoryServiceService_ListRepositoryDistributions_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/PreviewRepositoryPackageInstall", + Func: RepositoryServiceService_PreviewRepositoryPackageInstall_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/ConfirmRepositoryPackageInstall", + Func: RepositoryServiceService_ConfirmRepositoryPackageInstall_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/DescribeRepositoryPackageInstall", + Func: RepositoryServiceService_DescribeRepositoryPackageInstall_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/ExportRepositoryInventory", + Func: RepositoryServiceService_ExportRepositoryInventory_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/PreviewRepositoryInventoryImport", + Func: RepositoryServiceService_PreviewRepositoryInventoryImport_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/ConfirmRepositoryInventoryImport", + Func: RepositoryServiceService_ConfirmRepositoryInventoryImport_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/ExportRepositoryValidationReport", + Func: RepositoryServiceService_ExportRepositoryValidationReport_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/ExportRepositoryDistributionResult", + Func: RepositoryServiceService_ExportRepositoryDistributionResult_Handler, + }, + { + Name: "/trpc.ocmanager.repository.RepositoryService/MigrateRepository", + Func: RepositoryServiceService_MigrateRepository_Handler, + }, + }, +} + +// RegisterRepositoryServiceService registers service. +func RegisterRepositoryServiceService(s server.Service, svr RepositoryServiceService) { + if err := s.Register(&RepositoryServiceServer_ServiceDesc, svr); err != nil { + panic(fmt.Sprintf("RepositoryService register error:%v", err)) + } +} + +// START --------------------------------- Default Unimplemented Server Service --------------------------------- START + +type UnimplementedRepositoryService struct{} + +func (s *UnimplementedRepositoryService) CreateRepository(ctx context.Context, req *CreateRepositoryRequest) (*RepositoryResponse, error) { + return nil, errors.New("rpc CreateRepository of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) UpdateRepository(ctx context.Context, req *UpdateRepositoryRequest) (*RepositoryResponse, error) { + return nil, errors.New("rpc UpdateRepository of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) DescribeRepository(ctx context.Context, req *DescribeRepositoryRequest) (*RepositoryResponse, error) { + return nil, errors.New("rpc DescribeRepository of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) ListRepositories(ctx context.Context, req *ListRepositoriesRequest) (*ListRepositoriesResponse, error) { + return nil, errors.New("rpc ListRepositories of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) DeleteRepository(ctx context.Context, req *DeleteRepositoryRequest) (*emptypb.Empty, error) { + return nil, errors.New("rpc DeleteRepository of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) BatchApplyRepositories(ctx context.Context, req *BatchApplyRepositoriesRequest) (*BatchApplyRepositoriesResponse, error) { + return nil, errors.New("rpc BatchApplyRepositories of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) CreateRepositoryUpstreamProfile(ctx context.Context, req *CreateRepositoryUpstreamProfileRequest) (*RepositoryUpstreamProfileResponse, error) { + return nil, errors.New("rpc CreateRepositoryUpstreamProfile of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) UpdateRepositoryUpstreamProfile(ctx context.Context, req *UpdateRepositoryUpstreamProfileRequest) (*RepositoryUpstreamProfileResponse, error) { + return nil, errors.New("rpc UpdateRepositoryUpstreamProfile of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) DescribeRepositoryUpstreamProfile(ctx context.Context, req *DescribeRepositoryUpstreamProfileRequest) (*RepositoryUpstreamProfileResponse, error) { + return nil, errors.New("rpc DescribeRepositoryUpstreamProfile of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) ListRepositoryUpstreamProfiles(ctx context.Context, req *ListRepositoryUpstreamProfilesRequest) (*ListRepositoryUpstreamProfilesResponse, error) { + return nil, errors.New("rpc ListRepositoryUpstreamProfiles of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) DeleteRepositoryUpstreamProfile(ctx context.Context, req *DeleteRepositoryUpstreamProfileRequest) (*emptypb.Empty, error) { + return nil, errors.New("rpc DeleteRepositoryUpstreamProfile of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) CreateRepositoryStorageNode(ctx context.Context, req *CreateRepositoryStorageNodeRequest) (*RepositoryStorageNodeResponse, error) { + return nil, errors.New("rpc CreateRepositoryStorageNode of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) UpdateRepositoryStorageNode(ctx context.Context, req *UpdateRepositoryStorageNodeRequest) (*RepositoryStorageNodeResponse, error) { + return nil, errors.New("rpc UpdateRepositoryStorageNode of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) DescribeRepositoryStorageNode(ctx context.Context, req *DescribeRepositoryStorageNodeRequest) (*RepositoryStorageNodeResponse, error) { + return nil, errors.New("rpc DescribeRepositoryStorageNode of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) ListRepositoryStorageNodes(ctx context.Context, req *ListRepositoryStorageNodesRequest) (*ListRepositoryStorageNodesResponse, error) { + return nil, errors.New("rpc ListRepositoryStorageNodes of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) DeleteRepositoryStorageNode(ctx context.Context, req *DeleteRepositoryStorageNodeRequest) (*emptypb.Empty, error) { + return nil, errors.New("rpc DeleteRepositoryStorageNode of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) ProbeRepositoryStorageNode(ctx context.Context, req *ProbeRepositoryStorageNodeRequest) (*RepositoryStorageNodeProbeResponse, error) { + return nil, errors.New("rpc ProbeRepositoryStorageNode of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) ListRepositoryPreflightRecords(ctx context.Context, req *ListRepositoryPreflightRecordsRequest) (*ListRepositoryPreflightRecordsResponse, error) { + return nil, errors.New("rpc ListRepositoryPreflightRecords of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) CreateRepositoryNodeEnrollment(ctx context.Context, req *CreateRepositoryNodeEnrollmentRequest) (*RepositoryNodeEnrollmentResponse, error) { + return nil, errors.New("rpc CreateRepositoryNodeEnrollment of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) RegisterRepositoryNodeArtifact(ctx context.Context, req *RegisterRepositoryNodeArtifactRequest) (*RepositoryNodeArtifactResponse, error) { + return nil, errors.New("rpc RegisterRepositoryNodeArtifact of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) ListRepositoryNodeArtifacts(ctx context.Context, req *ListRepositoryNodeArtifactsRequest) (*ListRepositoryNodeArtifactsResponse, error) { + return nil, errors.New("rpc ListRepositoryNodeArtifacts of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) DownloadRepositoryNodeArtifact(ctx context.Context, req *DownloadRepositoryNodeArtifactRequest) (*emptypb.Empty, error) { + return nil, errors.New("rpc DownloadRepositoryNodeArtifact of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) ConfigureRepositoryNodeInstallSource(ctx context.Context, req *ConfigureRepositoryNodeInstallSourceRequest) (*RepositoryNodeInstallSourceResponse, error) { + return nil, errors.New("rpc ConfigureRepositoryNodeInstallSource of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) ListRepositoryNodeInstallSources(ctx context.Context, req *ListRepositoryNodeInstallSourcesRequest) (*ListRepositoryNodeInstallSourcesResponse, error) { + return nil, errors.New("rpc ListRepositoryNodeInstallSources of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) UpsertRepositoryNodeRegistrationAllowlist(ctx context.Context, req *UpsertRepositoryNodeRegistrationAllowlistRequest) (*RepositoryNodeRegistrationAllowlistResponse, error) { + return nil, errors.New("rpc UpsertRepositoryNodeRegistrationAllowlist of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) ListRepositoryNodeRegistrationAllowlists(ctx context.Context, req *ListRepositoryNodeRegistrationAllowlistsRequest) (*ListRepositoryNodeRegistrationAllowlistsResponse, error) { + return nil, errors.New("rpc ListRepositoryNodeRegistrationAllowlists of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) SyncRepository(ctx context.Context, req *SyncRepositoryRequest) (*RepositoryTaskResponse, error) { + return nil, errors.New("rpc SyncRepository of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) UpsertRepositorySyncSchedule(ctx context.Context, req *UpsertRepositorySyncScheduleRequest) (*RepositorySyncScheduleResponse, error) { + return nil, errors.New("rpc UpsertRepositorySyncSchedule of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) DescribeRepositorySyncSchedule(ctx context.Context, req *DescribeRepositorySyncScheduleRequest) (*RepositorySyncScheduleResponse, error) { + return nil, errors.New("rpc DescribeRepositorySyncSchedule of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) PauseRepositorySyncSchedule(ctx context.Context, req *ChangeRepositorySyncScheduleStatusRequest) (*RepositorySyncScheduleResponse, error) { + return nil, errors.New("rpc PauseRepositorySyncSchedule of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) ResumeRepositorySyncSchedule(ctx context.Context, req *ChangeRepositorySyncScheduleStatusRequest) (*RepositorySyncScheduleResponse, error) { + return nil, errors.New("rpc ResumeRepositorySyncSchedule of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) GenerateRepositoryConfig(ctx context.Context, req *GenerateRepositoryConfigRequest) (*RepositoryConfigResponse, error) { + return nil, errors.New("rpc GenerateRepositoryConfig of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) RegisterRepositoryGPGKey(ctx context.Context, req *RegisterRepositoryGPGKeyRequest) (*RepositoryGPGKeyResponse, error) { + return nil, errors.New("rpc RegisterRepositoryGPGKey of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) ListRepositoryGPGKeys(ctx context.Context, req *ListRepositoryGPGKeysRequest) (*ListRepositoryGPGKeysResponse, error) { + return nil, errors.New("rpc ListRepositoryGPGKeys of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) DeleteRepositoryGPGKey(ctx context.Context, req *DeleteRepositoryGPGKeyRequest) (*emptypb.Empty, error) { + return nil, errors.New("rpc DeleteRepositoryGPGKey of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) DescribeRepositoryTask(ctx context.Context, req *DescribeRepositoryTaskRequest) (*RepositoryTaskResponse, error) { + return nil, errors.New("rpc DescribeRepositoryTask of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) ListRepositoryTasks(ctx context.Context, req *ListRepositoryTasksRequest) (*ListRepositoryTasksResponse, error) { + return nil, errors.New("rpc ListRepositoryTasks of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) CancelRepositoryTask(ctx context.Context, req *CancelRepositoryTaskRequest) (*RepositoryTaskResponse, error) { + return nil, errors.New("rpc CancelRepositoryTask of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) RetryRepositoryTask(ctx context.Context, req *RetryRepositoryTaskRequest) (*RepositoryTaskResponse, error) { + return nil, errors.New("rpc RetryRepositoryTask of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) ExportRepositoryTaskDiagnosis(ctx context.Context, req *ExportRepositoryTaskDiagnosisRequest) (*RepositoryDiagnosisReportResponse, error) { + return nil, errors.New("rpc ExportRepositoryTaskDiagnosis of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) DescribeRepositoryTaskLog(ctx context.Context, req *DescribeRepositoryTaskLogRequest) (*RepositoryTaskLogResponse, error) { + return nil, errors.New("rpc DescribeRepositoryTaskLog of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) DownloadRepositoryTaskLog(ctx context.Context, req *DownloadRepositoryTaskLogRequest) (*emptypb.Empty, error) { + return nil, errors.New("rpc DownloadRepositoryTaskLog of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) BuildRepositoryOCAIContext(ctx context.Context, req *BuildRepositoryOCAIContextRequest) (*RepositoryOCAIContextResponse, error) { + return nil, errors.New("rpc BuildRepositoryOCAIContext of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) CreateRepositoryOCAIAnalysis(ctx context.Context, req *CreateRepositoryOCAIAnalysisRequest) (*RepositoryOCAIAnalysisResponse, error) { + return nil, errors.New("rpc CreateRepositoryOCAIAnalysis of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) DescribeRepositoryOCAIAnalysis(ctx context.Context, req *DescribeRepositoryOCAIAnalysisRequest) (*RepositoryOCAIAnalysisResponse, error) { + return nil, errors.New("rpc DescribeRepositoryOCAIAnalysis of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) AppendRepositoryOCAIFeedback(ctx context.Context, req *AppendRepositoryOCAIFeedbackRequest) (*RepositoryOCAIAnalysisResponse, error) { + return nil, errors.New("rpc AppendRepositoryOCAIFeedback of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) BeginRepositoryUpload(ctx context.Context, req *BeginRepositoryUploadRequest) (*RepositoryUploadResponse, error) { + return nil, errors.New("rpc BeginRepositoryUpload of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) AppendRepositoryUploadChunk(ctx context.Context, req *AppendRepositoryUploadChunkRequest) (*RepositoryUploadResponse, error) { + return nil, errors.New("rpc AppendRepositoryUploadChunk of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) PutRepositoryUploadChunk(ctx context.Context, req *PutRepositoryUploadChunkRequest) (*PutRepositoryUploadChunkResponse, error) { + return nil, errors.New("rpc PutRepositoryUploadChunk of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) DescribeRepositoryUploadChunks(ctx context.Context, req *DescribeRepositoryUploadRequest) (*DescribeRepositoryUploadChunksResponse, error) { + return nil, errors.New("rpc DescribeRepositoryUploadChunks of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) PauseRepositoryUpload(ctx context.Context, req *DescribeRepositoryUploadRequest) (*RepositoryUploadResponse, error) { + return nil, errors.New("rpc PauseRepositoryUpload of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) ResumeRepositoryUpload(ctx context.Context, req *DescribeRepositoryUploadRequest) (*RepositoryUploadResponse, error) { + return nil, errors.New("rpc ResumeRepositoryUpload of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) CompleteRepositoryUpload(ctx context.Context, req *CompleteRepositoryUploadRequest) (*CompleteRepositoryUploadResponse, error) { + return nil, errors.New("rpc CompleteRepositoryUpload of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) AbortRepositoryUpload(ctx context.Context, req *AbortRepositoryUploadRequest) (*RepositoryUploadResponse, error) { + return nil, errors.New("rpc AbortRepositoryUpload of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) DescribeRepositoryUpload(ctx context.Context, req *DescribeRepositoryUploadRequest) (*RepositoryUploadResponse, error) { + return nil, errors.New("rpc DescribeRepositoryUpload of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) ListRepositoryPackages(ctx context.Context, req *ListRepositoryPackagesRequest) (*ListRepositoryPackagesResponse, error) { + return nil, errors.New("rpc ListRepositoryPackages of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) ListRepositoryQuarantine(ctx context.Context, req *ListRepositoryQuarantineRequest) (*ListRepositoryQuarantineResponse, error) { + return nil, errors.New("rpc ListRepositoryQuarantine of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) DeleteRepositoryPackage(ctx context.Context, req *DeleteRepositoryPackageRequest) (*RepositoryTaskResponse, error) { + return nil, errors.New("rpc DeleteRepositoryPackage of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) PreviewRepositoryPackageDeletion(ctx context.Context, req *PreviewRepositoryPackageDeletionRequest) (*RepositoryPackageDeletionPreviewResponse, error) { + return nil, errors.New("rpc PreviewRepositoryPackageDeletion of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) ConfirmRepositoryPackageDeletion(ctx context.Context, req *ConfirmRepositoryPackageDeletionRequest) (*RepositoryTaskResponse, error) { + return nil, errors.New("rpc ConfirmRepositoryPackageDeletion of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) ListRepositoryDistributionNodes(ctx context.Context, req *ListRepositoryDistributionNodesRequest) (*ListRepositoryDistributionNodesResponse, error) { + return nil, errors.New("rpc ListRepositoryDistributionNodes of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) CreateRepositoryDistribution(ctx context.Context, req *CreateRepositoryDistributionRequest) (*RepositoryDistributionResponse, error) { + return nil, errors.New("rpc CreateRepositoryDistribution of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) DescribeRepositoryDistribution(ctx context.Context, req *DescribeRepositoryDistributionRequest) (*RepositoryDistributionResponse, error) { + return nil, errors.New("rpc DescribeRepositoryDistribution of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) ListRepositoryDistributions(ctx context.Context, req *ListRepositoryDistributionsRequest) (*ListRepositoryDistributionsResponse, error) { + return nil, errors.New("rpc ListRepositoryDistributions of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) PreviewRepositoryPackageInstall(ctx context.Context, req *PreviewRepositoryPackageInstallRequest) (*RepositoryPackageInstallPreviewResponse, error) { + return nil, errors.New("rpc PreviewRepositoryPackageInstall of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) ConfirmRepositoryPackageInstall(ctx context.Context, req *ConfirmRepositoryPackageInstallRequest) (*RepositoryPackageInstallResponse, error) { + return nil, errors.New("rpc ConfirmRepositoryPackageInstall of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) DescribeRepositoryPackageInstall(ctx context.Context, req *DescribeRepositoryPackageInstallRequest) (*RepositoryPackageInstallResponse, error) { + return nil, errors.New("rpc DescribeRepositoryPackageInstall of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) ExportRepositoryInventory(ctx context.Context, req *ExportRepositoryInventoryRequest) (*RepositoryExportResponse, error) { + return nil, errors.New("rpc ExportRepositoryInventory of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) PreviewRepositoryInventoryImport(ctx context.Context, req *PreviewRepositoryInventoryImportRequest) (*RepositoryInventoryImportPreviewResponse, error) { + return nil, errors.New("rpc PreviewRepositoryInventoryImport of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) ConfirmRepositoryInventoryImport(ctx context.Context, req *ConfirmRepositoryInventoryImportRequest) (*RepositoryInventoryImportResponse, error) { + return nil, errors.New("rpc ConfirmRepositoryInventoryImport of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) ExportRepositoryValidationReport(ctx context.Context, req *ExportRepositoryValidationReportRequest) (*RepositoryExportResponse, error) { + return nil, errors.New("rpc ExportRepositoryValidationReport of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) ExportRepositoryDistributionResult(ctx context.Context, req *ExportRepositoryDistributionResultRequest) (*RepositoryExportResponse, error) { + return nil, errors.New("rpc ExportRepositoryDistributionResult of service RepositoryService is not implemented") +} +func (s *UnimplementedRepositoryService) MigrateRepository(ctx context.Context, req *MigrateRepositoryRequest) (*RepositoryTaskResponse, error) { + return nil, errors.New("rpc MigrateRepository of service RepositoryService is not implemented") +} + +// END --------------------------------- Default Unimplemented Server Service --------------------------------- END + +// END ======================================= Server Service Definition ======================================= END + +// START ======================================= Client Service Definition ======================================= START + +// RepositoryServiceClientProxy defines service client proxy +type RepositoryServiceClientProxy interface { + CreateRepository(ctx context.Context, req *CreateRepositoryRequest, opts ...client.Option) (rsp *RepositoryResponse, err error) + + UpdateRepository(ctx context.Context, req *UpdateRepositoryRequest, opts ...client.Option) (rsp *RepositoryResponse, err error) + + DescribeRepository(ctx context.Context, req *DescribeRepositoryRequest, opts ...client.Option) (rsp *RepositoryResponse, err error) + + ListRepositories(ctx context.Context, req *ListRepositoriesRequest, opts ...client.Option) (rsp *ListRepositoriesResponse, err error) + + DeleteRepository(ctx context.Context, req *DeleteRepositoryRequest, opts ...client.Option) (rsp *emptypb.Empty, err error) + + BatchApplyRepositories(ctx context.Context, req *BatchApplyRepositoriesRequest, opts ...client.Option) (rsp *BatchApplyRepositoriesResponse, err error) + + CreateRepositoryUpstreamProfile(ctx context.Context, req *CreateRepositoryUpstreamProfileRequest, opts ...client.Option) (rsp *RepositoryUpstreamProfileResponse, err error) + + UpdateRepositoryUpstreamProfile(ctx context.Context, req *UpdateRepositoryUpstreamProfileRequest, opts ...client.Option) (rsp *RepositoryUpstreamProfileResponse, err error) + + DescribeRepositoryUpstreamProfile(ctx context.Context, req *DescribeRepositoryUpstreamProfileRequest, opts ...client.Option) (rsp *RepositoryUpstreamProfileResponse, err error) + + ListRepositoryUpstreamProfiles(ctx context.Context, req *ListRepositoryUpstreamProfilesRequest, opts ...client.Option) (rsp *ListRepositoryUpstreamProfilesResponse, err error) + + DeleteRepositoryUpstreamProfile(ctx context.Context, req *DeleteRepositoryUpstreamProfileRequest, opts ...client.Option) (rsp *emptypb.Empty, err error) + + CreateRepositoryStorageNode(ctx context.Context, req *CreateRepositoryStorageNodeRequest, opts ...client.Option) (rsp *RepositoryStorageNodeResponse, err error) + + UpdateRepositoryStorageNode(ctx context.Context, req *UpdateRepositoryStorageNodeRequest, opts ...client.Option) (rsp *RepositoryStorageNodeResponse, err error) + + DescribeRepositoryStorageNode(ctx context.Context, req *DescribeRepositoryStorageNodeRequest, opts ...client.Option) (rsp *RepositoryStorageNodeResponse, err error) + + ListRepositoryStorageNodes(ctx context.Context, req *ListRepositoryStorageNodesRequest, opts ...client.Option) (rsp *ListRepositoryStorageNodesResponse, err error) + + DeleteRepositoryStorageNode(ctx context.Context, req *DeleteRepositoryStorageNodeRequest, opts ...client.Option) (rsp *emptypb.Empty, err error) + + ProbeRepositoryStorageNode(ctx context.Context, req *ProbeRepositoryStorageNodeRequest, opts ...client.Option) (rsp *RepositoryStorageNodeProbeResponse, err error) + + ListRepositoryPreflightRecords(ctx context.Context, req *ListRepositoryPreflightRecordsRequest, opts ...client.Option) (rsp *ListRepositoryPreflightRecordsResponse, err error) + + CreateRepositoryNodeEnrollment(ctx context.Context, req *CreateRepositoryNodeEnrollmentRequest, opts ...client.Option) (rsp *RepositoryNodeEnrollmentResponse, err error) + + RegisterRepositoryNodeArtifact(ctx context.Context, req *RegisterRepositoryNodeArtifactRequest, opts ...client.Option) (rsp *RepositoryNodeArtifactResponse, err error) + + ListRepositoryNodeArtifacts(ctx context.Context, req *ListRepositoryNodeArtifactsRequest, opts ...client.Option) (rsp *ListRepositoryNodeArtifactsResponse, err error) + + DownloadRepositoryNodeArtifact(ctx context.Context, req *DownloadRepositoryNodeArtifactRequest, opts ...client.Option) (rsp *emptypb.Empty, err error) + + ConfigureRepositoryNodeInstallSource(ctx context.Context, req *ConfigureRepositoryNodeInstallSourceRequest, opts ...client.Option) (rsp *RepositoryNodeInstallSourceResponse, err error) + + ListRepositoryNodeInstallSources(ctx context.Context, req *ListRepositoryNodeInstallSourcesRequest, opts ...client.Option) (rsp *ListRepositoryNodeInstallSourcesResponse, err error) + + UpsertRepositoryNodeRegistrationAllowlist(ctx context.Context, req *UpsertRepositoryNodeRegistrationAllowlistRequest, opts ...client.Option) (rsp *RepositoryNodeRegistrationAllowlistResponse, err error) + + ListRepositoryNodeRegistrationAllowlists(ctx context.Context, req *ListRepositoryNodeRegistrationAllowlistsRequest, opts ...client.Option) (rsp *ListRepositoryNodeRegistrationAllowlistsResponse, err error) + + SyncRepository(ctx context.Context, req *SyncRepositoryRequest, opts ...client.Option) (rsp *RepositoryTaskResponse, err error) + + UpsertRepositorySyncSchedule(ctx context.Context, req *UpsertRepositorySyncScheduleRequest, opts ...client.Option) (rsp *RepositorySyncScheduleResponse, err error) + + DescribeRepositorySyncSchedule(ctx context.Context, req *DescribeRepositorySyncScheduleRequest, opts ...client.Option) (rsp *RepositorySyncScheduleResponse, err error) + + PauseRepositorySyncSchedule(ctx context.Context, req *ChangeRepositorySyncScheduleStatusRequest, opts ...client.Option) (rsp *RepositorySyncScheduleResponse, err error) + + ResumeRepositorySyncSchedule(ctx context.Context, req *ChangeRepositorySyncScheduleStatusRequest, opts ...client.Option) (rsp *RepositorySyncScheduleResponse, err error) + + GenerateRepositoryConfig(ctx context.Context, req *GenerateRepositoryConfigRequest, opts ...client.Option) (rsp *RepositoryConfigResponse, err error) + + RegisterRepositoryGPGKey(ctx context.Context, req *RegisterRepositoryGPGKeyRequest, opts ...client.Option) (rsp *RepositoryGPGKeyResponse, err error) + + ListRepositoryGPGKeys(ctx context.Context, req *ListRepositoryGPGKeysRequest, opts ...client.Option) (rsp *ListRepositoryGPGKeysResponse, err error) + + DeleteRepositoryGPGKey(ctx context.Context, req *DeleteRepositoryGPGKeyRequest, opts ...client.Option) (rsp *emptypb.Empty, err error) + + DescribeRepositoryTask(ctx context.Context, req *DescribeRepositoryTaskRequest, opts ...client.Option) (rsp *RepositoryTaskResponse, err error) + + ListRepositoryTasks(ctx context.Context, req *ListRepositoryTasksRequest, opts ...client.Option) (rsp *ListRepositoryTasksResponse, err error) + + CancelRepositoryTask(ctx context.Context, req *CancelRepositoryTaskRequest, opts ...client.Option) (rsp *RepositoryTaskResponse, err error) + + RetryRepositoryTask(ctx context.Context, req *RetryRepositoryTaskRequest, opts ...client.Option) (rsp *RepositoryTaskResponse, err error) + + ExportRepositoryTaskDiagnosis(ctx context.Context, req *ExportRepositoryTaskDiagnosisRequest, opts ...client.Option) (rsp *RepositoryDiagnosisReportResponse, err error) + + DescribeRepositoryTaskLog(ctx context.Context, req *DescribeRepositoryTaskLogRequest, opts ...client.Option) (rsp *RepositoryTaskLogResponse, err error) + + DownloadRepositoryTaskLog(ctx context.Context, req *DownloadRepositoryTaskLogRequest, opts ...client.Option) (rsp *emptypb.Empty, err error) + + BuildRepositoryOCAIContext(ctx context.Context, req *BuildRepositoryOCAIContextRequest, opts ...client.Option) (rsp *RepositoryOCAIContextResponse, err error) + + CreateRepositoryOCAIAnalysis(ctx context.Context, req *CreateRepositoryOCAIAnalysisRequest, opts ...client.Option) (rsp *RepositoryOCAIAnalysisResponse, err error) + + DescribeRepositoryOCAIAnalysis(ctx context.Context, req *DescribeRepositoryOCAIAnalysisRequest, opts ...client.Option) (rsp *RepositoryOCAIAnalysisResponse, err error) + + AppendRepositoryOCAIFeedback(ctx context.Context, req *AppendRepositoryOCAIFeedbackRequest, opts ...client.Option) (rsp *RepositoryOCAIAnalysisResponse, err error) + + BeginRepositoryUpload(ctx context.Context, req *BeginRepositoryUploadRequest, opts ...client.Option) (rsp *RepositoryUploadResponse, err error) + + AppendRepositoryUploadChunk(ctx context.Context, req *AppendRepositoryUploadChunkRequest, opts ...client.Option) (rsp *RepositoryUploadResponse, err error) + + PutRepositoryUploadChunk(ctx context.Context, req *PutRepositoryUploadChunkRequest, opts ...client.Option) (rsp *PutRepositoryUploadChunkResponse, err error) + + DescribeRepositoryUploadChunks(ctx context.Context, req *DescribeRepositoryUploadRequest, opts ...client.Option) (rsp *DescribeRepositoryUploadChunksResponse, err error) + + PauseRepositoryUpload(ctx context.Context, req *DescribeRepositoryUploadRequest, opts ...client.Option) (rsp *RepositoryUploadResponse, err error) + + ResumeRepositoryUpload(ctx context.Context, req *DescribeRepositoryUploadRequest, opts ...client.Option) (rsp *RepositoryUploadResponse, err error) + + CompleteRepositoryUpload(ctx context.Context, req *CompleteRepositoryUploadRequest, opts ...client.Option) (rsp *CompleteRepositoryUploadResponse, err error) + + AbortRepositoryUpload(ctx context.Context, req *AbortRepositoryUploadRequest, opts ...client.Option) (rsp *RepositoryUploadResponse, err error) + + DescribeRepositoryUpload(ctx context.Context, req *DescribeRepositoryUploadRequest, opts ...client.Option) (rsp *RepositoryUploadResponse, err error) + + ListRepositoryPackages(ctx context.Context, req *ListRepositoryPackagesRequest, opts ...client.Option) (rsp *ListRepositoryPackagesResponse, err error) + + ListRepositoryQuarantine(ctx context.Context, req *ListRepositoryQuarantineRequest, opts ...client.Option) (rsp *ListRepositoryQuarantineResponse, err error) + + DeleteRepositoryPackage(ctx context.Context, req *DeleteRepositoryPackageRequest, opts ...client.Option) (rsp *RepositoryTaskResponse, err error) + + PreviewRepositoryPackageDeletion(ctx context.Context, req *PreviewRepositoryPackageDeletionRequest, opts ...client.Option) (rsp *RepositoryPackageDeletionPreviewResponse, err error) + + ConfirmRepositoryPackageDeletion(ctx context.Context, req *ConfirmRepositoryPackageDeletionRequest, opts ...client.Option) (rsp *RepositoryTaskResponse, err error) + + ListRepositoryDistributionNodes(ctx context.Context, req *ListRepositoryDistributionNodesRequest, opts ...client.Option) (rsp *ListRepositoryDistributionNodesResponse, err error) + + CreateRepositoryDistribution(ctx context.Context, req *CreateRepositoryDistributionRequest, opts ...client.Option) (rsp *RepositoryDistributionResponse, err error) + + DescribeRepositoryDistribution(ctx context.Context, req *DescribeRepositoryDistributionRequest, opts ...client.Option) (rsp *RepositoryDistributionResponse, err error) + + ListRepositoryDistributions(ctx context.Context, req *ListRepositoryDistributionsRequest, opts ...client.Option) (rsp *ListRepositoryDistributionsResponse, err error) + + PreviewRepositoryPackageInstall(ctx context.Context, req *PreviewRepositoryPackageInstallRequest, opts ...client.Option) (rsp *RepositoryPackageInstallPreviewResponse, err error) + + ConfirmRepositoryPackageInstall(ctx context.Context, req *ConfirmRepositoryPackageInstallRequest, opts ...client.Option) (rsp *RepositoryPackageInstallResponse, err error) + + DescribeRepositoryPackageInstall(ctx context.Context, req *DescribeRepositoryPackageInstallRequest, opts ...client.Option) (rsp *RepositoryPackageInstallResponse, err error) + + ExportRepositoryInventory(ctx context.Context, req *ExportRepositoryInventoryRequest, opts ...client.Option) (rsp *RepositoryExportResponse, err error) + + PreviewRepositoryInventoryImport(ctx context.Context, req *PreviewRepositoryInventoryImportRequest, opts ...client.Option) (rsp *RepositoryInventoryImportPreviewResponse, err error) + + ConfirmRepositoryInventoryImport(ctx context.Context, req *ConfirmRepositoryInventoryImportRequest, opts ...client.Option) (rsp *RepositoryInventoryImportResponse, err error) + + ExportRepositoryValidationReport(ctx context.Context, req *ExportRepositoryValidationReportRequest, opts ...client.Option) (rsp *RepositoryExportResponse, err error) + + ExportRepositoryDistributionResult(ctx context.Context, req *ExportRepositoryDistributionResultRequest, opts ...client.Option) (rsp *RepositoryExportResponse, err error) + + MigrateRepository(ctx context.Context, req *MigrateRepositoryRequest, opts ...client.Option) (rsp *RepositoryTaskResponse, err error) +} + +type RepositoryServiceClientProxyImpl struct { + client client.Client + opts []client.Option +} + +var NewRepositoryServiceClientProxy = func(opts ...client.Option) RepositoryServiceClientProxy { + return &RepositoryServiceClientProxyImpl{client: client.DefaultClient, opts: opts} +} + +func (c *RepositoryServiceClientProxyImpl) CreateRepository(ctx context.Context, req *CreateRepositoryRequest, opts ...client.Option) (*RepositoryResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/CreateRepository") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("CreateRepository") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) UpdateRepository(ctx context.Context, req *UpdateRepositoryRequest, opts ...client.Option) (*RepositoryResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/UpdateRepository") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("UpdateRepository") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) DescribeRepository(ctx context.Context, req *DescribeRepositoryRequest, opts ...client.Option) (*RepositoryResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/DescribeRepository") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("DescribeRepository") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) ListRepositories(ctx context.Context, req *ListRepositoriesRequest, opts ...client.Option) (*ListRepositoriesResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/ListRepositories") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("ListRepositories") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &ListRepositoriesResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) DeleteRepository(ctx context.Context, req *DeleteRepositoryRequest, opts ...client.Option) (*emptypb.Empty, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/DeleteRepository") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("DeleteRepository") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &emptypb.Empty{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) BatchApplyRepositories(ctx context.Context, req *BatchApplyRepositoriesRequest, opts ...client.Option) (*BatchApplyRepositoriesResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/BatchApplyRepositories") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("BatchApplyRepositories") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &BatchApplyRepositoriesResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) CreateRepositoryUpstreamProfile(ctx context.Context, req *CreateRepositoryUpstreamProfileRequest, opts ...client.Option) (*RepositoryUpstreamProfileResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/CreateRepositoryUpstreamProfile") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("CreateRepositoryUpstreamProfile") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryUpstreamProfileResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) UpdateRepositoryUpstreamProfile(ctx context.Context, req *UpdateRepositoryUpstreamProfileRequest, opts ...client.Option) (*RepositoryUpstreamProfileResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/UpdateRepositoryUpstreamProfile") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("UpdateRepositoryUpstreamProfile") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryUpstreamProfileResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) DescribeRepositoryUpstreamProfile(ctx context.Context, req *DescribeRepositoryUpstreamProfileRequest, opts ...client.Option) (*RepositoryUpstreamProfileResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/DescribeRepositoryUpstreamProfile") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("DescribeRepositoryUpstreamProfile") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryUpstreamProfileResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) ListRepositoryUpstreamProfiles(ctx context.Context, req *ListRepositoryUpstreamProfilesRequest, opts ...client.Option) (*ListRepositoryUpstreamProfilesResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/ListRepositoryUpstreamProfiles") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("ListRepositoryUpstreamProfiles") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &ListRepositoryUpstreamProfilesResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) DeleteRepositoryUpstreamProfile(ctx context.Context, req *DeleteRepositoryUpstreamProfileRequest, opts ...client.Option) (*emptypb.Empty, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/DeleteRepositoryUpstreamProfile") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("DeleteRepositoryUpstreamProfile") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &emptypb.Empty{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) CreateRepositoryStorageNode(ctx context.Context, req *CreateRepositoryStorageNodeRequest, opts ...client.Option) (*RepositoryStorageNodeResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/CreateRepositoryStorageNode") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("CreateRepositoryStorageNode") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryStorageNodeResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) UpdateRepositoryStorageNode(ctx context.Context, req *UpdateRepositoryStorageNodeRequest, opts ...client.Option) (*RepositoryStorageNodeResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/UpdateRepositoryStorageNode") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("UpdateRepositoryStorageNode") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryStorageNodeResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) DescribeRepositoryStorageNode(ctx context.Context, req *DescribeRepositoryStorageNodeRequest, opts ...client.Option) (*RepositoryStorageNodeResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/DescribeRepositoryStorageNode") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("DescribeRepositoryStorageNode") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryStorageNodeResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) ListRepositoryStorageNodes(ctx context.Context, req *ListRepositoryStorageNodesRequest, opts ...client.Option) (*ListRepositoryStorageNodesResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/ListRepositoryStorageNodes") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("ListRepositoryStorageNodes") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &ListRepositoryStorageNodesResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) DeleteRepositoryStorageNode(ctx context.Context, req *DeleteRepositoryStorageNodeRequest, opts ...client.Option) (*emptypb.Empty, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/DeleteRepositoryStorageNode") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("DeleteRepositoryStorageNode") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &emptypb.Empty{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) ProbeRepositoryStorageNode(ctx context.Context, req *ProbeRepositoryStorageNodeRequest, opts ...client.Option) (*RepositoryStorageNodeProbeResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/ProbeRepositoryStorageNode") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("ProbeRepositoryStorageNode") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryStorageNodeProbeResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) ListRepositoryPreflightRecords(ctx context.Context, req *ListRepositoryPreflightRecordsRequest, opts ...client.Option) (*ListRepositoryPreflightRecordsResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/ListRepositoryPreflightRecords") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("ListRepositoryPreflightRecords") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &ListRepositoryPreflightRecordsResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) CreateRepositoryNodeEnrollment(ctx context.Context, req *CreateRepositoryNodeEnrollmentRequest, opts ...client.Option) (*RepositoryNodeEnrollmentResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/CreateRepositoryNodeEnrollment") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("CreateRepositoryNodeEnrollment") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryNodeEnrollmentResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) RegisterRepositoryNodeArtifact(ctx context.Context, req *RegisterRepositoryNodeArtifactRequest, opts ...client.Option) (*RepositoryNodeArtifactResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/RegisterRepositoryNodeArtifact") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("RegisterRepositoryNodeArtifact") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryNodeArtifactResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) ListRepositoryNodeArtifacts(ctx context.Context, req *ListRepositoryNodeArtifactsRequest, opts ...client.Option) (*ListRepositoryNodeArtifactsResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/ListRepositoryNodeArtifacts") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("ListRepositoryNodeArtifacts") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &ListRepositoryNodeArtifactsResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) DownloadRepositoryNodeArtifact(ctx context.Context, req *DownloadRepositoryNodeArtifactRequest, opts ...client.Option) (*emptypb.Empty, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/DownloadRepositoryNodeArtifact") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("DownloadRepositoryNodeArtifact") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &emptypb.Empty{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) ConfigureRepositoryNodeInstallSource(ctx context.Context, req *ConfigureRepositoryNodeInstallSourceRequest, opts ...client.Option) (*RepositoryNodeInstallSourceResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/ConfigureRepositoryNodeInstallSource") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("ConfigureRepositoryNodeInstallSource") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryNodeInstallSourceResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) ListRepositoryNodeInstallSources(ctx context.Context, req *ListRepositoryNodeInstallSourcesRequest, opts ...client.Option) (*ListRepositoryNodeInstallSourcesResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/ListRepositoryNodeInstallSources") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("ListRepositoryNodeInstallSources") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &ListRepositoryNodeInstallSourcesResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) UpsertRepositoryNodeRegistrationAllowlist(ctx context.Context, req *UpsertRepositoryNodeRegistrationAllowlistRequest, opts ...client.Option) (*RepositoryNodeRegistrationAllowlistResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/UpsertRepositoryNodeRegistrationAllowlist") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("UpsertRepositoryNodeRegistrationAllowlist") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryNodeRegistrationAllowlistResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) ListRepositoryNodeRegistrationAllowlists(ctx context.Context, req *ListRepositoryNodeRegistrationAllowlistsRequest, opts ...client.Option) (*ListRepositoryNodeRegistrationAllowlistsResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/ListRepositoryNodeRegistrationAllowlists") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("ListRepositoryNodeRegistrationAllowlists") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &ListRepositoryNodeRegistrationAllowlistsResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) SyncRepository(ctx context.Context, req *SyncRepositoryRequest, opts ...client.Option) (*RepositoryTaskResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/SyncRepository") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("SyncRepository") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryTaskResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) UpsertRepositorySyncSchedule(ctx context.Context, req *UpsertRepositorySyncScheduleRequest, opts ...client.Option) (*RepositorySyncScheduleResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/UpsertRepositorySyncSchedule") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("UpsertRepositorySyncSchedule") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositorySyncScheduleResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) DescribeRepositorySyncSchedule(ctx context.Context, req *DescribeRepositorySyncScheduleRequest, opts ...client.Option) (*RepositorySyncScheduleResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/DescribeRepositorySyncSchedule") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("DescribeRepositorySyncSchedule") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositorySyncScheduleResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) PauseRepositorySyncSchedule(ctx context.Context, req *ChangeRepositorySyncScheduleStatusRequest, opts ...client.Option) (*RepositorySyncScheduleResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/PauseRepositorySyncSchedule") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("PauseRepositorySyncSchedule") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositorySyncScheduleResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) ResumeRepositorySyncSchedule(ctx context.Context, req *ChangeRepositorySyncScheduleStatusRequest, opts ...client.Option) (*RepositorySyncScheduleResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/ResumeRepositorySyncSchedule") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("ResumeRepositorySyncSchedule") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositorySyncScheduleResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) GenerateRepositoryConfig(ctx context.Context, req *GenerateRepositoryConfigRequest, opts ...client.Option) (*RepositoryConfigResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/GenerateRepositoryConfig") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("GenerateRepositoryConfig") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryConfigResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) RegisterRepositoryGPGKey(ctx context.Context, req *RegisterRepositoryGPGKeyRequest, opts ...client.Option) (*RepositoryGPGKeyResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/RegisterRepositoryGPGKey") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("RegisterRepositoryGPGKey") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryGPGKeyResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) ListRepositoryGPGKeys(ctx context.Context, req *ListRepositoryGPGKeysRequest, opts ...client.Option) (*ListRepositoryGPGKeysResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/ListRepositoryGPGKeys") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("ListRepositoryGPGKeys") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &ListRepositoryGPGKeysResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) DeleteRepositoryGPGKey(ctx context.Context, req *DeleteRepositoryGPGKeyRequest, opts ...client.Option) (*emptypb.Empty, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/DeleteRepositoryGPGKey") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("DeleteRepositoryGPGKey") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &emptypb.Empty{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) DescribeRepositoryTask(ctx context.Context, req *DescribeRepositoryTaskRequest, opts ...client.Option) (*RepositoryTaskResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/DescribeRepositoryTask") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("DescribeRepositoryTask") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryTaskResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) ListRepositoryTasks(ctx context.Context, req *ListRepositoryTasksRequest, opts ...client.Option) (*ListRepositoryTasksResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/ListRepositoryTasks") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("ListRepositoryTasks") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &ListRepositoryTasksResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) CancelRepositoryTask(ctx context.Context, req *CancelRepositoryTaskRequest, opts ...client.Option) (*RepositoryTaskResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/CancelRepositoryTask") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("CancelRepositoryTask") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryTaskResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) RetryRepositoryTask(ctx context.Context, req *RetryRepositoryTaskRequest, opts ...client.Option) (*RepositoryTaskResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/RetryRepositoryTask") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("RetryRepositoryTask") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryTaskResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) ExportRepositoryTaskDiagnosis(ctx context.Context, req *ExportRepositoryTaskDiagnosisRequest, opts ...client.Option) (*RepositoryDiagnosisReportResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/ExportRepositoryTaskDiagnosis") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("ExportRepositoryTaskDiagnosis") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryDiagnosisReportResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) DescribeRepositoryTaskLog(ctx context.Context, req *DescribeRepositoryTaskLogRequest, opts ...client.Option) (*RepositoryTaskLogResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/DescribeRepositoryTaskLog") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("DescribeRepositoryTaskLog") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryTaskLogResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) DownloadRepositoryTaskLog(ctx context.Context, req *DownloadRepositoryTaskLogRequest, opts ...client.Option) (*emptypb.Empty, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/DownloadRepositoryTaskLog") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("DownloadRepositoryTaskLog") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &emptypb.Empty{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) BuildRepositoryOCAIContext(ctx context.Context, req *BuildRepositoryOCAIContextRequest, opts ...client.Option) (*RepositoryOCAIContextResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/BuildRepositoryOCAIContext") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("BuildRepositoryOCAIContext") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryOCAIContextResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) CreateRepositoryOCAIAnalysis(ctx context.Context, req *CreateRepositoryOCAIAnalysisRequest, opts ...client.Option) (*RepositoryOCAIAnalysisResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/CreateRepositoryOCAIAnalysis") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("CreateRepositoryOCAIAnalysis") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryOCAIAnalysisResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) DescribeRepositoryOCAIAnalysis(ctx context.Context, req *DescribeRepositoryOCAIAnalysisRequest, opts ...client.Option) (*RepositoryOCAIAnalysisResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/DescribeRepositoryOCAIAnalysis") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("DescribeRepositoryOCAIAnalysis") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryOCAIAnalysisResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) AppendRepositoryOCAIFeedback(ctx context.Context, req *AppendRepositoryOCAIFeedbackRequest, opts ...client.Option) (*RepositoryOCAIAnalysisResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/AppendRepositoryOCAIFeedback") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("AppendRepositoryOCAIFeedback") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryOCAIAnalysisResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) BeginRepositoryUpload(ctx context.Context, req *BeginRepositoryUploadRequest, opts ...client.Option) (*RepositoryUploadResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/BeginRepositoryUpload") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("BeginRepositoryUpload") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryUploadResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) AppendRepositoryUploadChunk(ctx context.Context, req *AppendRepositoryUploadChunkRequest, opts ...client.Option) (*RepositoryUploadResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/AppendRepositoryUploadChunk") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("AppendRepositoryUploadChunk") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryUploadResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) PutRepositoryUploadChunk(ctx context.Context, req *PutRepositoryUploadChunkRequest, opts ...client.Option) (*PutRepositoryUploadChunkResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/PutRepositoryUploadChunk") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("PutRepositoryUploadChunk") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &PutRepositoryUploadChunkResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) DescribeRepositoryUploadChunks(ctx context.Context, req *DescribeRepositoryUploadRequest, opts ...client.Option) (*DescribeRepositoryUploadChunksResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/DescribeRepositoryUploadChunks") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("DescribeRepositoryUploadChunks") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &DescribeRepositoryUploadChunksResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) PauseRepositoryUpload(ctx context.Context, req *DescribeRepositoryUploadRequest, opts ...client.Option) (*RepositoryUploadResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/PauseRepositoryUpload") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("PauseRepositoryUpload") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryUploadResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) ResumeRepositoryUpload(ctx context.Context, req *DescribeRepositoryUploadRequest, opts ...client.Option) (*RepositoryUploadResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/ResumeRepositoryUpload") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("ResumeRepositoryUpload") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryUploadResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) CompleteRepositoryUpload(ctx context.Context, req *CompleteRepositoryUploadRequest, opts ...client.Option) (*CompleteRepositoryUploadResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/CompleteRepositoryUpload") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("CompleteRepositoryUpload") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &CompleteRepositoryUploadResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) AbortRepositoryUpload(ctx context.Context, req *AbortRepositoryUploadRequest, opts ...client.Option) (*RepositoryUploadResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/AbortRepositoryUpload") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("AbortRepositoryUpload") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryUploadResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) DescribeRepositoryUpload(ctx context.Context, req *DescribeRepositoryUploadRequest, opts ...client.Option) (*RepositoryUploadResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/DescribeRepositoryUpload") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("DescribeRepositoryUpload") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryUploadResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) ListRepositoryPackages(ctx context.Context, req *ListRepositoryPackagesRequest, opts ...client.Option) (*ListRepositoryPackagesResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/ListRepositoryPackages") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("ListRepositoryPackages") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &ListRepositoryPackagesResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) ListRepositoryQuarantine(ctx context.Context, req *ListRepositoryQuarantineRequest, opts ...client.Option) (*ListRepositoryQuarantineResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/ListRepositoryQuarantine") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("ListRepositoryQuarantine") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &ListRepositoryQuarantineResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) DeleteRepositoryPackage(ctx context.Context, req *DeleteRepositoryPackageRequest, opts ...client.Option) (*RepositoryTaskResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/DeleteRepositoryPackage") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("DeleteRepositoryPackage") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryTaskResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) PreviewRepositoryPackageDeletion(ctx context.Context, req *PreviewRepositoryPackageDeletionRequest, opts ...client.Option) (*RepositoryPackageDeletionPreviewResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/PreviewRepositoryPackageDeletion") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("PreviewRepositoryPackageDeletion") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryPackageDeletionPreviewResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) ConfirmRepositoryPackageDeletion(ctx context.Context, req *ConfirmRepositoryPackageDeletionRequest, opts ...client.Option) (*RepositoryTaskResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/ConfirmRepositoryPackageDeletion") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("ConfirmRepositoryPackageDeletion") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryTaskResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) ListRepositoryDistributionNodes(ctx context.Context, req *ListRepositoryDistributionNodesRequest, opts ...client.Option) (*ListRepositoryDistributionNodesResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/ListRepositoryDistributionNodes") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("ListRepositoryDistributionNodes") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &ListRepositoryDistributionNodesResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) CreateRepositoryDistribution(ctx context.Context, req *CreateRepositoryDistributionRequest, opts ...client.Option) (*RepositoryDistributionResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/CreateRepositoryDistribution") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("CreateRepositoryDistribution") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryDistributionResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) DescribeRepositoryDistribution(ctx context.Context, req *DescribeRepositoryDistributionRequest, opts ...client.Option) (*RepositoryDistributionResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/DescribeRepositoryDistribution") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("DescribeRepositoryDistribution") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryDistributionResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) ListRepositoryDistributions(ctx context.Context, req *ListRepositoryDistributionsRequest, opts ...client.Option) (*ListRepositoryDistributionsResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/ListRepositoryDistributions") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("ListRepositoryDistributions") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &ListRepositoryDistributionsResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) PreviewRepositoryPackageInstall(ctx context.Context, req *PreviewRepositoryPackageInstallRequest, opts ...client.Option) (*RepositoryPackageInstallPreviewResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/PreviewRepositoryPackageInstall") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("PreviewRepositoryPackageInstall") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryPackageInstallPreviewResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) ConfirmRepositoryPackageInstall(ctx context.Context, req *ConfirmRepositoryPackageInstallRequest, opts ...client.Option) (*RepositoryPackageInstallResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/ConfirmRepositoryPackageInstall") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("ConfirmRepositoryPackageInstall") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryPackageInstallResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) DescribeRepositoryPackageInstall(ctx context.Context, req *DescribeRepositoryPackageInstallRequest, opts ...client.Option) (*RepositoryPackageInstallResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/DescribeRepositoryPackageInstall") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("DescribeRepositoryPackageInstall") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryPackageInstallResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) ExportRepositoryInventory(ctx context.Context, req *ExportRepositoryInventoryRequest, opts ...client.Option) (*RepositoryExportResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/ExportRepositoryInventory") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("ExportRepositoryInventory") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryExportResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) PreviewRepositoryInventoryImport(ctx context.Context, req *PreviewRepositoryInventoryImportRequest, opts ...client.Option) (*RepositoryInventoryImportPreviewResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/PreviewRepositoryInventoryImport") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("PreviewRepositoryInventoryImport") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryInventoryImportPreviewResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) ConfirmRepositoryInventoryImport(ctx context.Context, req *ConfirmRepositoryInventoryImportRequest, opts ...client.Option) (*RepositoryInventoryImportResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/ConfirmRepositoryInventoryImport") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("ConfirmRepositoryInventoryImport") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryInventoryImportResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) ExportRepositoryValidationReport(ctx context.Context, req *ExportRepositoryValidationReportRequest, opts ...client.Option) (*RepositoryExportResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/ExportRepositoryValidationReport") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("ExportRepositoryValidationReport") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryExportResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) ExportRepositoryDistributionResult(ctx context.Context, req *ExportRepositoryDistributionResultRequest, opts ...client.Option) (*RepositoryExportResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/ExportRepositoryDistributionResult") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("ExportRepositoryDistributionResult") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryExportResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryServiceClientProxyImpl) MigrateRepository(ctx context.Context, req *MigrateRepositoryRequest, opts ...client.Option) (*RepositoryTaskResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repository.RepositoryService/MigrateRepository") + msg.WithCalleeServiceName(RepositoryServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repository") + msg.WithCalleeService("RepositoryService") + msg.WithCalleeMethod("MigrateRepository") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &RepositoryTaskResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +// END ======================================= Client Service Definition ======================================= END diff --git a/manager/backend/protocol/repository/repository_mock.go b/manager/backend/protocol/repository/repository_mock.go new file mode 100644 index 0000000000000000000000000000000000000000..955c3c2ceeda6960bb319b5dcd3b039f462b7402 --- /dev/null +++ b/manager/backend/protocol/repository/repository_mock.go @@ -0,0 +1,2615 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: protocol/repository/repository.trpc.go + +// Package repository is a generated GoMock package. +package repository + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + emptypb "google.golang.org/protobuf/types/known/emptypb" + client "trpc.group/trpc-go/trpc-go/client" +) + +// MockRepositoryServiceService is a mock of RepositoryServiceService interface. +type MockRepositoryServiceService struct { + ctrl *gomock.Controller + recorder *MockRepositoryServiceServiceMockRecorder +} + +// MockRepositoryServiceServiceMockRecorder is the mock recorder for MockRepositoryServiceService. +type MockRepositoryServiceServiceMockRecorder struct { + mock *MockRepositoryServiceService +} + +// NewMockRepositoryServiceService creates a new mock instance. +func NewMockRepositoryServiceService(ctrl *gomock.Controller) *MockRepositoryServiceService { + mock := &MockRepositoryServiceService{ctrl: ctrl} + mock.recorder = &MockRepositoryServiceServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepositoryServiceService) EXPECT() *MockRepositoryServiceServiceMockRecorder { + return m.recorder +} + +// AbortRepositoryUpload mocks base method. +func (m *MockRepositoryServiceService) AbortRepositoryUpload(ctx context.Context, req *AbortRepositoryUploadRequest) (*RepositoryUploadResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AbortRepositoryUpload", ctx, req) + ret0, _ := ret[0].(*RepositoryUploadResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AbortRepositoryUpload indicates an expected call of AbortRepositoryUpload. +func (mr *MockRepositoryServiceServiceMockRecorder) AbortRepositoryUpload(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AbortRepositoryUpload", reflect.TypeOf((*MockRepositoryServiceService)(nil).AbortRepositoryUpload), ctx, req) +} + +// AppendRepositoryOCAIFeedback mocks base method. +func (m *MockRepositoryServiceService) AppendRepositoryOCAIFeedback(ctx context.Context, req *AppendRepositoryOCAIFeedbackRequest) (*RepositoryOCAIAnalysisResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AppendRepositoryOCAIFeedback", ctx, req) + ret0, _ := ret[0].(*RepositoryOCAIAnalysisResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AppendRepositoryOCAIFeedback indicates an expected call of AppendRepositoryOCAIFeedback. +func (mr *MockRepositoryServiceServiceMockRecorder) AppendRepositoryOCAIFeedback(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendRepositoryOCAIFeedback", reflect.TypeOf((*MockRepositoryServiceService)(nil).AppendRepositoryOCAIFeedback), ctx, req) +} + +// AppendRepositoryUploadChunk mocks base method. +func (m *MockRepositoryServiceService) AppendRepositoryUploadChunk(ctx context.Context, req *AppendRepositoryUploadChunkRequest) (*RepositoryUploadResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AppendRepositoryUploadChunk", ctx, req) + ret0, _ := ret[0].(*RepositoryUploadResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AppendRepositoryUploadChunk indicates an expected call of AppendRepositoryUploadChunk. +func (mr *MockRepositoryServiceServiceMockRecorder) AppendRepositoryUploadChunk(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendRepositoryUploadChunk", reflect.TypeOf((*MockRepositoryServiceService)(nil).AppendRepositoryUploadChunk), ctx, req) +} + +// BatchApplyRepositories mocks base method. +func (m *MockRepositoryServiceService) BatchApplyRepositories(ctx context.Context, req *BatchApplyRepositoriesRequest) (*BatchApplyRepositoriesResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BatchApplyRepositories", ctx, req) + ret0, _ := ret[0].(*BatchApplyRepositoriesResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BatchApplyRepositories indicates an expected call of BatchApplyRepositories. +func (mr *MockRepositoryServiceServiceMockRecorder) BatchApplyRepositories(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchApplyRepositories", reflect.TypeOf((*MockRepositoryServiceService)(nil).BatchApplyRepositories), ctx, req) +} + +// BeginRepositoryUpload mocks base method. +func (m *MockRepositoryServiceService) BeginRepositoryUpload(ctx context.Context, req *BeginRepositoryUploadRequest) (*RepositoryUploadResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BeginRepositoryUpload", ctx, req) + ret0, _ := ret[0].(*RepositoryUploadResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BeginRepositoryUpload indicates an expected call of BeginRepositoryUpload. +func (mr *MockRepositoryServiceServiceMockRecorder) BeginRepositoryUpload(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BeginRepositoryUpload", reflect.TypeOf((*MockRepositoryServiceService)(nil).BeginRepositoryUpload), ctx, req) +} + +// BuildRepositoryOCAIContext mocks base method. +func (m *MockRepositoryServiceService) BuildRepositoryOCAIContext(ctx context.Context, req *BuildRepositoryOCAIContextRequest) (*RepositoryOCAIContextResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BuildRepositoryOCAIContext", ctx, req) + ret0, _ := ret[0].(*RepositoryOCAIContextResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BuildRepositoryOCAIContext indicates an expected call of BuildRepositoryOCAIContext. +func (mr *MockRepositoryServiceServiceMockRecorder) BuildRepositoryOCAIContext(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildRepositoryOCAIContext", reflect.TypeOf((*MockRepositoryServiceService)(nil).BuildRepositoryOCAIContext), ctx, req) +} + +// CancelRepositoryTask mocks base method. +func (m *MockRepositoryServiceService) CancelRepositoryTask(ctx context.Context, req *CancelRepositoryTaskRequest) (*RepositoryTaskResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CancelRepositoryTask", ctx, req) + ret0, _ := ret[0].(*RepositoryTaskResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CancelRepositoryTask indicates an expected call of CancelRepositoryTask. +func (mr *MockRepositoryServiceServiceMockRecorder) CancelRepositoryTask(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CancelRepositoryTask", reflect.TypeOf((*MockRepositoryServiceService)(nil).CancelRepositoryTask), ctx, req) +} + +// CompleteRepositoryUpload mocks base method. +func (m *MockRepositoryServiceService) CompleteRepositoryUpload(ctx context.Context, req *CompleteRepositoryUploadRequest) (*CompleteRepositoryUploadResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CompleteRepositoryUpload", ctx, req) + ret0, _ := ret[0].(*CompleteRepositoryUploadResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CompleteRepositoryUpload indicates an expected call of CompleteRepositoryUpload. +func (mr *MockRepositoryServiceServiceMockRecorder) CompleteRepositoryUpload(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CompleteRepositoryUpload", reflect.TypeOf((*MockRepositoryServiceService)(nil).CompleteRepositoryUpload), ctx, req) +} + +// ConfigureRepositoryNodeInstallSource mocks base method. +func (m *MockRepositoryServiceService) ConfigureRepositoryNodeInstallSource(ctx context.Context, req *ConfigureRepositoryNodeInstallSourceRequest) (*RepositoryNodeInstallSourceResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConfigureRepositoryNodeInstallSource", ctx, req) + ret0, _ := ret[0].(*RepositoryNodeInstallSourceResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ConfigureRepositoryNodeInstallSource indicates an expected call of ConfigureRepositoryNodeInstallSource. +func (mr *MockRepositoryServiceServiceMockRecorder) ConfigureRepositoryNodeInstallSource(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigureRepositoryNodeInstallSource", reflect.TypeOf((*MockRepositoryServiceService)(nil).ConfigureRepositoryNodeInstallSource), ctx, req) +} + +// ConfirmRepositoryInventoryImport mocks base method. +func (m *MockRepositoryServiceService) ConfirmRepositoryInventoryImport(ctx context.Context, req *ConfirmRepositoryInventoryImportRequest) (*RepositoryInventoryImportResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConfirmRepositoryInventoryImport", ctx, req) + ret0, _ := ret[0].(*RepositoryInventoryImportResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ConfirmRepositoryInventoryImport indicates an expected call of ConfirmRepositoryInventoryImport. +func (mr *MockRepositoryServiceServiceMockRecorder) ConfirmRepositoryInventoryImport(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfirmRepositoryInventoryImport", reflect.TypeOf((*MockRepositoryServiceService)(nil).ConfirmRepositoryInventoryImport), ctx, req) +} + +// ConfirmRepositoryPackageDeletion mocks base method. +func (m *MockRepositoryServiceService) ConfirmRepositoryPackageDeletion(ctx context.Context, req *ConfirmRepositoryPackageDeletionRequest) (*RepositoryTaskResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConfirmRepositoryPackageDeletion", ctx, req) + ret0, _ := ret[0].(*RepositoryTaskResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ConfirmRepositoryPackageDeletion indicates an expected call of ConfirmRepositoryPackageDeletion. +func (mr *MockRepositoryServiceServiceMockRecorder) ConfirmRepositoryPackageDeletion(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfirmRepositoryPackageDeletion", reflect.TypeOf((*MockRepositoryServiceService)(nil).ConfirmRepositoryPackageDeletion), ctx, req) +} + +// ConfirmRepositoryPackageInstall mocks base method. +func (m *MockRepositoryServiceService) ConfirmRepositoryPackageInstall(ctx context.Context, req *ConfirmRepositoryPackageInstallRequest) (*RepositoryPackageInstallResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConfirmRepositoryPackageInstall", ctx, req) + ret0, _ := ret[0].(*RepositoryPackageInstallResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ConfirmRepositoryPackageInstall indicates an expected call of ConfirmRepositoryPackageInstall. +func (mr *MockRepositoryServiceServiceMockRecorder) ConfirmRepositoryPackageInstall(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfirmRepositoryPackageInstall", reflect.TypeOf((*MockRepositoryServiceService)(nil).ConfirmRepositoryPackageInstall), ctx, req) +} + +// CreateRepository mocks base method. +func (m *MockRepositoryServiceService) CreateRepository(ctx context.Context, req *CreateRepositoryRequest) (*RepositoryResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateRepository", ctx, req) + ret0, _ := ret[0].(*RepositoryResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateRepository indicates an expected call of CreateRepository. +func (mr *MockRepositoryServiceServiceMockRecorder) CreateRepository(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRepository", reflect.TypeOf((*MockRepositoryServiceService)(nil).CreateRepository), ctx, req) +} + +// CreateRepositoryDistribution mocks base method. +func (m *MockRepositoryServiceService) CreateRepositoryDistribution(ctx context.Context, req *CreateRepositoryDistributionRequest) (*RepositoryDistributionResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateRepositoryDistribution", ctx, req) + ret0, _ := ret[0].(*RepositoryDistributionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateRepositoryDistribution indicates an expected call of CreateRepositoryDistribution. +func (mr *MockRepositoryServiceServiceMockRecorder) CreateRepositoryDistribution(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRepositoryDistribution", reflect.TypeOf((*MockRepositoryServiceService)(nil).CreateRepositoryDistribution), ctx, req) +} + +// CreateRepositoryNodeEnrollment mocks base method. +func (m *MockRepositoryServiceService) CreateRepositoryNodeEnrollment(ctx context.Context, req *CreateRepositoryNodeEnrollmentRequest) (*RepositoryNodeEnrollmentResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateRepositoryNodeEnrollment", ctx, req) + ret0, _ := ret[0].(*RepositoryNodeEnrollmentResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateRepositoryNodeEnrollment indicates an expected call of CreateRepositoryNodeEnrollment. +func (mr *MockRepositoryServiceServiceMockRecorder) CreateRepositoryNodeEnrollment(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRepositoryNodeEnrollment", reflect.TypeOf((*MockRepositoryServiceService)(nil).CreateRepositoryNodeEnrollment), ctx, req) +} + +// CreateRepositoryOCAIAnalysis mocks base method. +func (m *MockRepositoryServiceService) CreateRepositoryOCAIAnalysis(ctx context.Context, req *CreateRepositoryOCAIAnalysisRequest) (*RepositoryOCAIAnalysisResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateRepositoryOCAIAnalysis", ctx, req) + ret0, _ := ret[0].(*RepositoryOCAIAnalysisResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateRepositoryOCAIAnalysis indicates an expected call of CreateRepositoryOCAIAnalysis. +func (mr *MockRepositoryServiceServiceMockRecorder) CreateRepositoryOCAIAnalysis(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRepositoryOCAIAnalysis", reflect.TypeOf((*MockRepositoryServiceService)(nil).CreateRepositoryOCAIAnalysis), ctx, req) +} + +// CreateRepositoryStorageNode mocks base method. +func (m *MockRepositoryServiceService) CreateRepositoryStorageNode(ctx context.Context, req *CreateRepositoryStorageNodeRequest) (*RepositoryStorageNodeResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateRepositoryStorageNode", ctx, req) + ret0, _ := ret[0].(*RepositoryStorageNodeResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateRepositoryStorageNode indicates an expected call of CreateRepositoryStorageNode. +func (mr *MockRepositoryServiceServiceMockRecorder) CreateRepositoryStorageNode(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRepositoryStorageNode", reflect.TypeOf((*MockRepositoryServiceService)(nil).CreateRepositoryStorageNode), ctx, req) +} + +// CreateRepositoryUpstreamProfile mocks base method. +func (m *MockRepositoryServiceService) CreateRepositoryUpstreamProfile(ctx context.Context, req *CreateRepositoryUpstreamProfileRequest) (*RepositoryUpstreamProfileResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateRepositoryUpstreamProfile", ctx, req) + ret0, _ := ret[0].(*RepositoryUpstreamProfileResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateRepositoryUpstreamProfile indicates an expected call of CreateRepositoryUpstreamProfile. +func (mr *MockRepositoryServiceServiceMockRecorder) CreateRepositoryUpstreamProfile(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRepositoryUpstreamProfile", reflect.TypeOf((*MockRepositoryServiceService)(nil).CreateRepositoryUpstreamProfile), ctx, req) +} + +// DeleteRepository mocks base method. +func (m *MockRepositoryServiceService) DeleteRepository(ctx context.Context, req *DeleteRepositoryRequest) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteRepository", ctx, req) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteRepository indicates an expected call of DeleteRepository. +func (mr *MockRepositoryServiceServiceMockRecorder) DeleteRepository(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRepository", reflect.TypeOf((*MockRepositoryServiceService)(nil).DeleteRepository), ctx, req) +} + +// DeleteRepositoryGPGKey mocks base method. +func (m *MockRepositoryServiceService) DeleteRepositoryGPGKey(ctx context.Context, req *DeleteRepositoryGPGKeyRequest) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteRepositoryGPGKey", ctx, req) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteRepositoryGPGKey indicates an expected call of DeleteRepositoryGPGKey. +func (mr *MockRepositoryServiceServiceMockRecorder) DeleteRepositoryGPGKey(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRepositoryGPGKey", reflect.TypeOf((*MockRepositoryServiceService)(nil).DeleteRepositoryGPGKey), ctx, req) +} + +// DeleteRepositoryPackage mocks base method. +func (m *MockRepositoryServiceService) DeleteRepositoryPackage(ctx context.Context, req *DeleteRepositoryPackageRequest) (*RepositoryTaskResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteRepositoryPackage", ctx, req) + ret0, _ := ret[0].(*RepositoryTaskResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteRepositoryPackage indicates an expected call of DeleteRepositoryPackage. +func (mr *MockRepositoryServiceServiceMockRecorder) DeleteRepositoryPackage(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRepositoryPackage", reflect.TypeOf((*MockRepositoryServiceService)(nil).DeleteRepositoryPackage), ctx, req) +} + +// DeleteRepositoryStorageNode mocks base method. +func (m *MockRepositoryServiceService) DeleteRepositoryStorageNode(ctx context.Context, req *DeleteRepositoryStorageNodeRequest) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteRepositoryStorageNode", ctx, req) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteRepositoryStorageNode indicates an expected call of DeleteRepositoryStorageNode. +func (mr *MockRepositoryServiceServiceMockRecorder) DeleteRepositoryStorageNode(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRepositoryStorageNode", reflect.TypeOf((*MockRepositoryServiceService)(nil).DeleteRepositoryStorageNode), ctx, req) +} + +// DeleteRepositoryUpstreamProfile mocks base method. +func (m *MockRepositoryServiceService) DeleteRepositoryUpstreamProfile(ctx context.Context, req *DeleteRepositoryUpstreamProfileRequest) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteRepositoryUpstreamProfile", ctx, req) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteRepositoryUpstreamProfile indicates an expected call of DeleteRepositoryUpstreamProfile. +func (mr *MockRepositoryServiceServiceMockRecorder) DeleteRepositoryUpstreamProfile(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRepositoryUpstreamProfile", reflect.TypeOf((*MockRepositoryServiceService)(nil).DeleteRepositoryUpstreamProfile), ctx, req) +} + +// DescribeRepository mocks base method. +func (m *MockRepositoryServiceService) DescribeRepository(ctx context.Context, req *DescribeRepositoryRequest) (*RepositoryResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DescribeRepository", ctx, req) + ret0, _ := ret[0].(*RepositoryResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeRepository indicates an expected call of DescribeRepository. +func (mr *MockRepositoryServiceServiceMockRecorder) DescribeRepository(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeRepository", reflect.TypeOf((*MockRepositoryServiceService)(nil).DescribeRepository), ctx, req) +} + +// DescribeRepositoryDistribution mocks base method. +func (m *MockRepositoryServiceService) DescribeRepositoryDistribution(ctx context.Context, req *DescribeRepositoryDistributionRequest) (*RepositoryDistributionResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DescribeRepositoryDistribution", ctx, req) + ret0, _ := ret[0].(*RepositoryDistributionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeRepositoryDistribution indicates an expected call of DescribeRepositoryDistribution. +func (mr *MockRepositoryServiceServiceMockRecorder) DescribeRepositoryDistribution(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeRepositoryDistribution", reflect.TypeOf((*MockRepositoryServiceService)(nil).DescribeRepositoryDistribution), ctx, req) +} + +// DescribeRepositoryOCAIAnalysis mocks base method. +func (m *MockRepositoryServiceService) DescribeRepositoryOCAIAnalysis(ctx context.Context, req *DescribeRepositoryOCAIAnalysisRequest) (*RepositoryOCAIAnalysisResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DescribeRepositoryOCAIAnalysis", ctx, req) + ret0, _ := ret[0].(*RepositoryOCAIAnalysisResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeRepositoryOCAIAnalysis indicates an expected call of DescribeRepositoryOCAIAnalysis. +func (mr *MockRepositoryServiceServiceMockRecorder) DescribeRepositoryOCAIAnalysis(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeRepositoryOCAIAnalysis", reflect.TypeOf((*MockRepositoryServiceService)(nil).DescribeRepositoryOCAIAnalysis), ctx, req) +} + +// DescribeRepositoryPackageInstall mocks base method. +func (m *MockRepositoryServiceService) DescribeRepositoryPackageInstall(ctx context.Context, req *DescribeRepositoryPackageInstallRequest) (*RepositoryPackageInstallResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DescribeRepositoryPackageInstall", ctx, req) + ret0, _ := ret[0].(*RepositoryPackageInstallResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeRepositoryPackageInstall indicates an expected call of DescribeRepositoryPackageInstall. +func (mr *MockRepositoryServiceServiceMockRecorder) DescribeRepositoryPackageInstall(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeRepositoryPackageInstall", reflect.TypeOf((*MockRepositoryServiceService)(nil).DescribeRepositoryPackageInstall), ctx, req) +} + +// DescribeRepositoryStorageNode mocks base method. +func (m *MockRepositoryServiceService) DescribeRepositoryStorageNode(ctx context.Context, req *DescribeRepositoryStorageNodeRequest) (*RepositoryStorageNodeResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DescribeRepositoryStorageNode", ctx, req) + ret0, _ := ret[0].(*RepositoryStorageNodeResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeRepositoryStorageNode indicates an expected call of DescribeRepositoryStorageNode. +func (mr *MockRepositoryServiceServiceMockRecorder) DescribeRepositoryStorageNode(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeRepositoryStorageNode", reflect.TypeOf((*MockRepositoryServiceService)(nil).DescribeRepositoryStorageNode), ctx, req) +} + +// DescribeRepositorySyncSchedule mocks base method. +func (m *MockRepositoryServiceService) DescribeRepositorySyncSchedule(ctx context.Context, req *DescribeRepositorySyncScheduleRequest) (*RepositorySyncScheduleResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DescribeRepositorySyncSchedule", ctx, req) + ret0, _ := ret[0].(*RepositorySyncScheduleResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeRepositorySyncSchedule indicates an expected call of DescribeRepositorySyncSchedule. +func (mr *MockRepositoryServiceServiceMockRecorder) DescribeRepositorySyncSchedule(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeRepositorySyncSchedule", reflect.TypeOf((*MockRepositoryServiceService)(nil).DescribeRepositorySyncSchedule), ctx, req) +} + +// DescribeRepositoryTask mocks base method. +func (m *MockRepositoryServiceService) DescribeRepositoryTask(ctx context.Context, req *DescribeRepositoryTaskRequest) (*RepositoryTaskResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DescribeRepositoryTask", ctx, req) + ret0, _ := ret[0].(*RepositoryTaskResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeRepositoryTask indicates an expected call of DescribeRepositoryTask. +func (mr *MockRepositoryServiceServiceMockRecorder) DescribeRepositoryTask(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeRepositoryTask", reflect.TypeOf((*MockRepositoryServiceService)(nil).DescribeRepositoryTask), ctx, req) +} + +// DescribeRepositoryTaskLog mocks base method. +func (m *MockRepositoryServiceService) DescribeRepositoryTaskLog(ctx context.Context, req *DescribeRepositoryTaskLogRequest) (*RepositoryTaskLogResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DescribeRepositoryTaskLog", ctx, req) + ret0, _ := ret[0].(*RepositoryTaskLogResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeRepositoryTaskLog indicates an expected call of DescribeRepositoryTaskLog. +func (mr *MockRepositoryServiceServiceMockRecorder) DescribeRepositoryTaskLog(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeRepositoryTaskLog", reflect.TypeOf((*MockRepositoryServiceService)(nil).DescribeRepositoryTaskLog), ctx, req) +} + +// DescribeRepositoryUpload mocks base method. +func (m *MockRepositoryServiceService) DescribeRepositoryUpload(ctx context.Context, req *DescribeRepositoryUploadRequest) (*RepositoryUploadResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DescribeRepositoryUpload", ctx, req) + ret0, _ := ret[0].(*RepositoryUploadResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeRepositoryUpload indicates an expected call of DescribeRepositoryUpload. +func (mr *MockRepositoryServiceServiceMockRecorder) DescribeRepositoryUpload(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeRepositoryUpload", reflect.TypeOf((*MockRepositoryServiceService)(nil).DescribeRepositoryUpload), ctx, req) +} + +// DescribeRepositoryUploadChunks mocks base method. +func (m *MockRepositoryServiceService) DescribeRepositoryUploadChunks(ctx context.Context, req *DescribeRepositoryUploadRequest) (*DescribeRepositoryUploadChunksResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DescribeRepositoryUploadChunks", ctx, req) + ret0, _ := ret[0].(*DescribeRepositoryUploadChunksResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeRepositoryUploadChunks indicates an expected call of DescribeRepositoryUploadChunks. +func (mr *MockRepositoryServiceServiceMockRecorder) DescribeRepositoryUploadChunks(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeRepositoryUploadChunks", reflect.TypeOf((*MockRepositoryServiceService)(nil).DescribeRepositoryUploadChunks), ctx, req) +} + +// DescribeRepositoryUpstreamProfile mocks base method. +func (m *MockRepositoryServiceService) DescribeRepositoryUpstreamProfile(ctx context.Context, req *DescribeRepositoryUpstreamProfileRequest) (*RepositoryUpstreamProfileResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DescribeRepositoryUpstreamProfile", ctx, req) + ret0, _ := ret[0].(*RepositoryUpstreamProfileResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeRepositoryUpstreamProfile indicates an expected call of DescribeRepositoryUpstreamProfile. +func (mr *MockRepositoryServiceServiceMockRecorder) DescribeRepositoryUpstreamProfile(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeRepositoryUpstreamProfile", reflect.TypeOf((*MockRepositoryServiceService)(nil).DescribeRepositoryUpstreamProfile), ctx, req) +} + +// DownloadRepositoryNodeArtifact mocks base method. +func (m *MockRepositoryServiceService) DownloadRepositoryNodeArtifact(ctx context.Context, req *DownloadRepositoryNodeArtifactRequest) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DownloadRepositoryNodeArtifact", ctx, req) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DownloadRepositoryNodeArtifact indicates an expected call of DownloadRepositoryNodeArtifact. +func (mr *MockRepositoryServiceServiceMockRecorder) DownloadRepositoryNodeArtifact(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DownloadRepositoryNodeArtifact", reflect.TypeOf((*MockRepositoryServiceService)(nil).DownloadRepositoryNodeArtifact), ctx, req) +} + +// DownloadRepositoryTaskLog mocks base method. +func (m *MockRepositoryServiceService) DownloadRepositoryTaskLog(ctx context.Context, req *DownloadRepositoryTaskLogRequest) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DownloadRepositoryTaskLog", ctx, req) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DownloadRepositoryTaskLog indicates an expected call of DownloadRepositoryTaskLog. +func (mr *MockRepositoryServiceServiceMockRecorder) DownloadRepositoryTaskLog(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DownloadRepositoryTaskLog", reflect.TypeOf((*MockRepositoryServiceService)(nil).DownloadRepositoryTaskLog), ctx, req) +} + +// ExportRepositoryDistributionResult mocks base method. +func (m *MockRepositoryServiceService) ExportRepositoryDistributionResult(ctx context.Context, req *ExportRepositoryDistributionResultRequest) (*RepositoryExportResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExportRepositoryDistributionResult", ctx, req) + ret0, _ := ret[0].(*RepositoryExportResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExportRepositoryDistributionResult indicates an expected call of ExportRepositoryDistributionResult. +func (mr *MockRepositoryServiceServiceMockRecorder) ExportRepositoryDistributionResult(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExportRepositoryDistributionResult", reflect.TypeOf((*MockRepositoryServiceService)(nil).ExportRepositoryDistributionResult), ctx, req) +} + +// ExportRepositoryInventory mocks base method. +func (m *MockRepositoryServiceService) ExportRepositoryInventory(ctx context.Context, req *ExportRepositoryInventoryRequest) (*RepositoryExportResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExportRepositoryInventory", ctx, req) + ret0, _ := ret[0].(*RepositoryExportResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExportRepositoryInventory indicates an expected call of ExportRepositoryInventory. +func (mr *MockRepositoryServiceServiceMockRecorder) ExportRepositoryInventory(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExportRepositoryInventory", reflect.TypeOf((*MockRepositoryServiceService)(nil).ExportRepositoryInventory), ctx, req) +} + +// ExportRepositoryTaskDiagnosis mocks base method. +func (m *MockRepositoryServiceService) ExportRepositoryTaskDiagnosis(ctx context.Context, req *ExportRepositoryTaskDiagnosisRequest) (*RepositoryDiagnosisReportResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExportRepositoryTaskDiagnosis", ctx, req) + ret0, _ := ret[0].(*RepositoryDiagnosisReportResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExportRepositoryTaskDiagnosis indicates an expected call of ExportRepositoryTaskDiagnosis. +func (mr *MockRepositoryServiceServiceMockRecorder) ExportRepositoryTaskDiagnosis(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExportRepositoryTaskDiagnosis", reflect.TypeOf((*MockRepositoryServiceService)(nil).ExportRepositoryTaskDiagnosis), ctx, req) +} + +// ExportRepositoryValidationReport mocks base method. +func (m *MockRepositoryServiceService) ExportRepositoryValidationReport(ctx context.Context, req *ExportRepositoryValidationReportRequest) (*RepositoryExportResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExportRepositoryValidationReport", ctx, req) + ret0, _ := ret[0].(*RepositoryExportResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExportRepositoryValidationReport indicates an expected call of ExportRepositoryValidationReport. +func (mr *MockRepositoryServiceServiceMockRecorder) ExportRepositoryValidationReport(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExportRepositoryValidationReport", reflect.TypeOf((*MockRepositoryServiceService)(nil).ExportRepositoryValidationReport), ctx, req) +} + +// GenerateRepositoryConfig mocks base method. +func (m *MockRepositoryServiceService) GenerateRepositoryConfig(ctx context.Context, req *GenerateRepositoryConfigRequest) (*RepositoryConfigResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GenerateRepositoryConfig", ctx, req) + ret0, _ := ret[0].(*RepositoryConfigResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GenerateRepositoryConfig indicates an expected call of GenerateRepositoryConfig. +func (mr *MockRepositoryServiceServiceMockRecorder) GenerateRepositoryConfig(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateRepositoryConfig", reflect.TypeOf((*MockRepositoryServiceService)(nil).GenerateRepositoryConfig), ctx, req) +} + +// ListRepositories mocks base method. +func (m *MockRepositoryServiceService) ListRepositories(ctx context.Context, req *ListRepositoriesRequest) (*ListRepositoriesResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListRepositories", ctx, req) + ret0, _ := ret[0].(*ListRepositoriesResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRepositories indicates an expected call of ListRepositories. +func (mr *MockRepositoryServiceServiceMockRecorder) ListRepositories(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRepositories", reflect.TypeOf((*MockRepositoryServiceService)(nil).ListRepositories), ctx, req) +} + +// ListRepositoryDistributionNodes mocks base method. +func (m *MockRepositoryServiceService) ListRepositoryDistributionNodes(ctx context.Context, req *ListRepositoryDistributionNodesRequest) (*ListRepositoryDistributionNodesResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListRepositoryDistributionNodes", ctx, req) + ret0, _ := ret[0].(*ListRepositoryDistributionNodesResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRepositoryDistributionNodes indicates an expected call of ListRepositoryDistributionNodes. +func (mr *MockRepositoryServiceServiceMockRecorder) ListRepositoryDistributionNodes(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRepositoryDistributionNodes", reflect.TypeOf((*MockRepositoryServiceService)(nil).ListRepositoryDistributionNodes), ctx, req) +} + +// ListRepositoryDistributions mocks base method. +func (m *MockRepositoryServiceService) ListRepositoryDistributions(ctx context.Context, req *ListRepositoryDistributionsRequest) (*ListRepositoryDistributionsResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListRepositoryDistributions", ctx, req) + ret0, _ := ret[0].(*ListRepositoryDistributionsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRepositoryDistributions indicates an expected call of ListRepositoryDistributions. +func (mr *MockRepositoryServiceServiceMockRecorder) ListRepositoryDistributions(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRepositoryDistributions", reflect.TypeOf((*MockRepositoryServiceService)(nil).ListRepositoryDistributions), ctx, req) +} + +// ListRepositoryGPGKeys mocks base method. +func (m *MockRepositoryServiceService) ListRepositoryGPGKeys(ctx context.Context, req *ListRepositoryGPGKeysRequest) (*ListRepositoryGPGKeysResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListRepositoryGPGKeys", ctx, req) + ret0, _ := ret[0].(*ListRepositoryGPGKeysResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRepositoryGPGKeys indicates an expected call of ListRepositoryGPGKeys. +func (mr *MockRepositoryServiceServiceMockRecorder) ListRepositoryGPGKeys(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRepositoryGPGKeys", reflect.TypeOf((*MockRepositoryServiceService)(nil).ListRepositoryGPGKeys), ctx, req) +} + +// ListRepositoryNodeArtifacts mocks base method. +func (m *MockRepositoryServiceService) ListRepositoryNodeArtifacts(ctx context.Context, req *ListRepositoryNodeArtifactsRequest) (*ListRepositoryNodeArtifactsResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListRepositoryNodeArtifacts", ctx, req) + ret0, _ := ret[0].(*ListRepositoryNodeArtifactsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRepositoryNodeArtifacts indicates an expected call of ListRepositoryNodeArtifacts. +func (mr *MockRepositoryServiceServiceMockRecorder) ListRepositoryNodeArtifacts(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRepositoryNodeArtifacts", reflect.TypeOf((*MockRepositoryServiceService)(nil).ListRepositoryNodeArtifacts), ctx, req) +} + +// ListRepositoryNodeInstallSources mocks base method. +func (m *MockRepositoryServiceService) ListRepositoryNodeInstallSources(ctx context.Context, req *ListRepositoryNodeInstallSourcesRequest) (*ListRepositoryNodeInstallSourcesResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListRepositoryNodeInstallSources", ctx, req) + ret0, _ := ret[0].(*ListRepositoryNodeInstallSourcesResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRepositoryNodeInstallSources indicates an expected call of ListRepositoryNodeInstallSources. +func (mr *MockRepositoryServiceServiceMockRecorder) ListRepositoryNodeInstallSources(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRepositoryNodeInstallSources", reflect.TypeOf((*MockRepositoryServiceService)(nil).ListRepositoryNodeInstallSources), ctx, req) +} + +// ListRepositoryNodeRegistrationAllowlists mocks base method. +func (m *MockRepositoryServiceService) ListRepositoryNodeRegistrationAllowlists(ctx context.Context, req *ListRepositoryNodeRegistrationAllowlistsRequest) (*ListRepositoryNodeRegistrationAllowlistsResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListRepositoryNodeRegistrationAllowlists", ctx, req) + ret0, _ := ret[0].(*ListRepositoryNodeRegistrationAllowlistsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRepositoryNodeRegistrationAllowlists indicates an expected call of ListRepositoryNodeRegistrationAllowlists. +func (mr *MockRepositoryServiceServiceMockRecorder) ListRepositoryNodeRegistrationAllowlists(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRepositoryNodeRegistrationAllowlists", reflect.TypeOf((*MockRepositoryServiceService)(nil).ListRepositoryNodeRegistrationAllowlists), ctx, req) +} + +// ListRepositoryPackages mocks base method. +func (m *MockRepositoryServiceService) ListRepositoryPackages(ctx context.Context, req *ListRepositoryPackagesRequest) (*ListRepositoryPackagesResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListRepositoryPackages", ctx, req) + ret0, _ := ret[0].(*ListRepositoryPackagesResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRepositoryPackages indicates an expected call of ListRepositoryPackages. +func (mr *MockRepositoryServiceServiceMockRecorder) ListRepositoryPackages(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRepositoryPackages", reflect.TypeOf((*MockRepositoryServiceService)(nil).ListRepositoryPackages), ctx, req) +} + +// ListRepositoryPreflightRecords mocks base method. +func (m *MockRepositoryServiceService) ListRepositoryPreflightRecords(ctx context.Context, req *ListRepositoryPreflightRecordsRequest) (*ListRepositoryPreflightRecordsResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListRepositoryPreflightRecords", ctx, req) + ret0, _ := ret[0].(*ListRepositoryPreflightRecordsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRepositoryPreflightRecords indicates an expected call of ListRepositoryPreflightRecords. +func (mr *MockRepositoryServiceServiceMockRecorder) ListRepositoryPreflightRecords(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRepositoryPreflightRecords", reflect.TypeOf((*MockRepositoryServiceService)(nil).ListRepositoryPreflightRecords), ctx, req) +} + +// ListRepositoryQuarantine mocks base method. +func (m *MockRepositoryServiceService) ListRepositoryQuarantine(ctx context.Context, req *ListRepositoryQuarantineRequest) (*ListRepositoryQuarantineResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListRepositoryQuarantine", ctx, req) + ret0, _ := ret[0].(*ListRepositoryQuarantineResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRepositoryQuarantine indicates an expected call of ListRepositoryQuarantine. +func (mr *MockRepositoryServiceServiceMockRecorder) ListRepositoryQuarantine(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRepositoryQuarantine", reflect.TypeOf((*MockRepositoryServiceService)(nil).ListRepositoryQuarantine), ctx, req) +} + +// ListRepositoryStorageNodes mocks base method. +func (m *MockRepositoryServiceService) ListRepositoryStorageNodes(ctx context.Context, req *ListRepositoryStorageNodesRequest) (*ListRepositoryStorageNodesResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListRepositoryStorageNodes", ctx, req) + ret0, _ := ret[0].(*ListRepositoryStorageNodesResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRepositoryStorageNodes indicates an expected call of ListRepositoryStorageNodes. +func (mr *MockRepositoryServiceServiceMockRecorder) ListRepositoryStorageNodes(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRepositoryStorageNodes", reflect.TypeOf((*MockRepositoryServiceService)(nil).ListRepositoryStorageNodes), ctx, req) +} + +// ListRepositoryTasks mocks base method. +func (m *MockRepositoryServiceService) ListRepositoryTasks(ctx context.Context, req *ListRepositoryTasksRequest) (*ListRepositoryTasksResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListRepositoryTasks", ctx, req) + ret0, _ := ret[0].(*ListRepositoryTasksResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRepositoryTasks indicates an expected call of ListRepositoryTasks. +func (mr *MockRepositoryServiceServiceMockRecorder) ListRepositoryTasks(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRepositoryTasks", reflect.TypeOf((*MockRepositoryServiceService)(nil).ListRepositoryTasks), ctx, req) +} + +// ListRepositoryUpstreamProfiles mocks base method. +func (m *MockRepositoryServiceService) ListRepositoryUpstreamProfiles(ctx context.Context, req *ListRepositoryUpstreamProfilesRequest) (*ListRepositoryUpstreamProfilesResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListRepositoryUpstreamProfiles", ctx, req) + ret0, _ := ret[0].(*ListRepositoryUpstreamProfilesResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRepositoryUpstreamProfiles indicates an expected call of ListRepositoryUpstreamProfiles. +func (mr *MockRepositoryServiceServiceMockRecorder) ListRepositoryUpstreamProfiles(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRepositoryUpstreamProfiles", reflect.TypeOf((*MockRepositoryServiceService)(nil).ListRepositoryUpstreamProfiles), ctx, req) +} + +// MigrateRepository mocks base method. +func (m *MockRepositoryServiceService) MigrateRepository(ctx context.Context, req *MigrateRepositoryRequest) (*RepositoryTaskResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MigrateRepository", ctx, req) + ret0, _ := ret[0].(*RepositoryTaskResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// MigrateRepository indicates an expected call of MigrateRepository. +func (mr *MockRepositoryServiceServiceMockRecorder) MigrateRepository(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MigrateRepository", reflect.TypeOf((*MockRepositoryServiceService)(nil).MigrateRepository), ctx, req) +} + +// PauseRepositorySyncSchedule mocks base method. +func (m *MockRepositoryServiceService) PauseRepositorySyncSchedule(ctx context.Context, req *ChangeRepositorySyncScheduleStatusRequest) (*RepositorySyncScheduleResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PauseRepositorySyncSchedule", ctx, req) + ret0, _ := ret[0].(*RepositorySyncScheduleResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PauseRepositorySyncSchedule indicates an expected call of PauseRepositorySyncSchedule. +func (mr *MockRepositoryServiceServiceMockRecorder) PauseRepositorySyncSchedule(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PauseRepositorySyncSchedule", reflect.TypeOf((*MockRepositoryServiceService)(nil).PauseRepositorySyncSchedule), ctx, req) +} + +// PauseRepositoryUpload mocks base method. +func (m *MockRepositoryServiceService) PauseRepositoryUpload(ctx context.Context, req *DescribeRepositoryUploadRequest) (*RepositoryUploadResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PauseRepositoryUpload", ctx, req) + ret0, _ := ret[0].(*RepositoryUploadResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PauseRepositoryUpload indicates an expected call of PauseRepositoryUpload. +func (mr *MockRepositoryServiceServiceMockRecorder) PauseRepositoryUpload(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PauseRepositoryUpload", reflect.TypeOf((*MockRepositoryServiceService)(nil).PauseRepositoryUpload), ctx, req) +} + +// PreviewRepositoryInventoryImport mocks base method. +func (m *MockRepositoryServiceService) PreviewRepositoryInventoryImport(ctx context.Context, req *PreviewRepositoryInventoryImportRequest) (*RepositoryInventoryImportPreviewResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PreviewRepositoryInventoryImport", ctx, req) + ret0, _ := ret[0].(*RepositoryInventoryImportPreviewResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PreviewRepositoryInventoryImport indicates an expected call of PreviewRepositoryInventoryImport. +func (mr *MockRepositoryServiceServiceMockRecorder) PreviewRepositoryInventoryImport(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PreviewRepositoryInventoryImport", reflect.TypeOf((*MockRepositoryServiceService)(nil).PreviewRepositoryInventoryImport), ctx, req) +} + +// PreviewRepositoryPackageDeletion mocks base method. +func (m *MockRepositoryServiceService) PreviewRepositoryPackageDeletion(ctx context.Context, req *PreviewRepositoryPackageDeletionRequest) (*RepositoryPackageDeletionPreviewResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PreviewRepositoryPackageDeletion", ctx, req) + ret0, _ := ret[0].(*RepositoryPackageDeletionPreviewResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PreviewRepositoryPackageDeletion indicates an expected call of PreviewRepositoryPackageDeletion. +func (mr *MockRepositoryServiceServiceMockRecorder) PreviewRepositoryPackageDeletion(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PreviewRepositoryPackageDeletion", reflect.TypeOf((*MockRepositoryServiceService)(nil).PreviewRepositoryPackageDeletion), ctx, req) +} + +// PreviewRepositoryPackageInstall mocks base method. +func (m *MockRepositoryServiceService) PreviewRepositoryPackageInstall(ctx context.Context, req *PreviewRepositoryPackageInstallRequest) (*RepositoryPackageInstallPreviewResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PreviewRepositoryPackageInstall", ctx, req) + ret0, _ := ret[0].(*RepositoryPackageInstallPreviewResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PreviewRepositoryPackageInstall indicates an expected call of PreviewRepositoryPackageInstall. +func (mr *MockRepositoryServiceServiceMockRecorder) PreviewRepositoryPackageInstall(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PreviewRepositoryPackageInstall", reflect.TypeOf((*MockRepositoryServiceService)(nil).PreviewRepositoryPackageInstall), ctx, req) +} + +// ProbeRepositoryStorageNode mocks base method. +func (m *MockRepositoryServiceService) ProbeRepositoryStorageNode(ctx context.Context, req *ProbeRepositoryStorageNodeRequest) (*RepositoryStorageNodeProbeResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ProbeRepositoryStorageNode", ctx, req) + ret0, _ := ret[0].(*RepositoryStorageNodeProbeResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ProbeRepositoryStorageNode indicates an expected call of ProbeRepositoryStorageNode. +func (mr *MockRepositoryServiceServiceMockRecorder) ProbeRepositoryStorageNode(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProbeRepositoryStorageNode", reflect.TypeOf((*MockRepositoryServiceService)(nil).ProbeRepositoryStorageNode), ctx, req) +} + +// PutRepositoryUploadChunk mocks base method. +func (m *MockRepositoryServiceService) PutRepositoryUploadChunk(ctx context.Context, req *PutRepositoryUploadChunkRequest) (*PutRepositoryUploadChunkResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PutRepositoryUploadChunk", ctx, req) + ret0, _ := ret[0].(*PutRepositoryUploadChunkResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PutRepositoryUploadChunk indicates an expected call of PutRepositoryUploadChunk. +func (mr *MockRepositoryServiceServiceMockRecorder) PutRepositoryUploadChunk(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutRepositoryUploadChunk", reflect.TypeOf((*MockRepositoryServiceService)(nil).PutRepositoryUploadChunk), ctx, req) +} + +// RegisterRepositoryGPGKey mocks base method. +func (m *MockRepositoryServiceService) RegisterRepositoryGPGKey(ctx context.Context, req *RegisterRepositoryGPGKeyRequest) (*RepositoryGPGKeyResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RegisterRepositoryGPGKey", ctx, req) + ret0, _ := ret[0].(*RepositoryGPGKeyResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RegisterRepositoryGPGKey indicates an expected call of RegisterRepositoryGPGKey. +func (mr *MockRepositoryServiceServiceMockRecorder) RegisterRepositoryGPGKey(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterRepositoryGPGKey", reflect.TypeOf((*MockRepositoryServiceService)(nil).RegisterRepositoryGPGKey), ctx, req) +} + +// RegisterRepositoryNodeArtifact mocks base method. +func (m *MockRepositoryServiceService) RegisterRepositoryNodeArtifact(ctx context.Context, req *RegisterRepositoryNodeArtifactRequest) (*RepositoryNodeArtifactResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RegisterRepositoryNodeArtifact", ctx, req) + ret0, _ := ret[0].(*RepositoryNodeArtifactResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RegisterRepositoryNodeArtifact indicates an expected call of RegisterRepositoryNodeArtifact. +func (mr *MockRepositoryServiceServiceMockRecorder) RegisterRepositoryNodeArtifact(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterRepositoryNodeArtifact", reflect.TypeOf((*MockRepositoryServiceService)(nil).RegisterRepositoryNodeArtifact), ctx, req) +} + +// ResumeRepositorySyncSchedule mocks base method. +func (m *MockRepositoryServiceService) ResumeRepositorySyncSchedule(ctx context.Context, req *ChangeRepositorySyncScheduleStatusRequest) (*RepositorySyncScheduleResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ResumeRepositorySyncSchedule", ctx, req) + ret0, _ := ret[0].(*RepositorySyncScheduleResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ResumeRepositorySyncSchedule indicates an expected call of ResumeRepositorySyncSchedule. +func (mr *MockRepositoryServiceServiceMockRecorder) ResumeRepositorySyncSchedule(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResumeRepositorySyncSchedule", reflect.TypeOf((*MockRepositoryServiceService)(nil).ResumeRepositorySyncSchedule), ctx, req) +} + +// ResumeRepositoryUpload mocks base method. +func (m *MockRepositoryServiceService) ResumeRepositoryUpload(ctx context.Context, req *DescribeRepositoryUploadRequest) (*RepositoryUploadResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ResumeRepositoryUpload", ctx, req) + ret0, _ := ret[0].(*RepositoryUploadResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ResumeRepositoryUpload indicates an expected call of ResumeRepositoryUpload. +func (mr *MockRepositoryServiceServiceMockRecorder) ResumeRepositoryUpload(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResumeRepositoryUpload", reflect.TypeOf((*MockRepositoryServiceService)(nil).ResumeRepositoryUpload), ctx, req) +} + +// RetryRepositoryTask mocks base method. +func (m *MockRepositoryServiceService) RetryRepositoryTask(ctx context.Context, req *RetryRepositoryTaskRequest) (*RepositoryTaskResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RetryRepositoryTask", ctx, req) + ret0, _ := ret[0].(*RepositoryTaskResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RetryRepositoryTask indicates an expected call of RetryRepositoryTask. +func (mr *MockRepositoryServiceServiceMockRecorder) RetryRepositoryTask(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RetryRepositoryTask", reflect.TypeOf((*MockRepositoryServiceService)(nil).RetryRepositoryTask), ctx, req) +} + +// SyncRepository mocks base method. +func (m *MockRepositoryServiceService) SyncRepository(ctx context.Context, req *SyncRepositoryRequest) (*RepositoryTaskResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SyncRepository", ctx, req) + ret0, _ := ret[0].(*RepositoryTaskResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SyncRepository indicates an expected call of SyncRepository. +func (mr *MockRepositoryServiceServiceMockRecorder) SyncRepository(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SyncRepository", reflect.TypeOf((*MockRepositoryServiceService)(nil).SyncRepository), ctx, req) +} + +// UpdateRepository mocks base method. +func (m *MockRepositoryServiceService) UpdateRepository(ctx context.Context, req *UpdateRepositoryRequest) (*RepositoryResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateRepository", ctx, req) + ret0, _ := ret[0].(*RepositoryResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateRepository indicates an expected call of UpdateRepository. +func (mr *MockRepositoryServiceServiceMockRecorder) UpdateRepository(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateRepository", reflect.TypeOf((*MockRepositoryServiceService)(nil).UpdateRepository), ctx, req) +} + +// UpdateRepositoryStorageNode mocks base method. +func (m *MockRepositoryServiceService) UpdateRepositoryStorageNode(ctx context.Context, req *UpdateRepositoryStorageNodeRequest) (*RepositoryStorageNodeResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateRepositoryStorageNode", ctx, req) + ret0, _ := ret[0].(*RepositoryStorageNodeResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateRepositoryStorageNode indicates an expected call of UpdateRepositoryStorageNode. +func (mr *MockRepositoryServiceServiceMockRecorder) UpdateRepositoryStorageNode(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateRepositoryStorageNode", reflect.TypeOf((*MockRepositoryServiceService)(nil).UpdateRepositoryStorageNode), ctx, req) +} + +// UpdateRepositoryUpstreamProfile mocks base method. +func (m *MockRepositoryServiceService) UpdateRepositoryUpstreamProfile(ctx context.Context, req *UpdateRepositoryUpstreamProfileRequest) (*RepositoryUpstreamProfileResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateRepositoryUpstreamProfile", ctx, req) + ret0, _ := ret[0].(*RepositoryUpstreamProfileResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateRepositoryUpstreamProfile indicates an expected call of UpdateRepositoryUpstreamProfile. +func (mr *MockRepositoryServiceServiceMockRecorder) UpdateRepositoryUpstreamProfile(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateRepositoryUpstreamProfile", reflect.TypeOf((*MockRepositoryServiceService)(nil).UpdateRepositoryUpstreamProfile), ctx, req) +} + +// UpsertRepositoryNodeRegistrationAllowlist mocks base method. +func (m *MockRepositoryServiceService) UpsertRepositoryNodeRegistrationAllowlist(ctx context.Context, req *UpsertRepositoryNodeRegistrationAllowlistRequest) (*RepositoryNodeRegistrationAllowlistResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpsertRepositoryNodeRegistrationAllowlist", ctx, req) + ret0, _ := ret[0].(*RepositoryNodeRegistrationAllowlistResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpsertRepositoryNodeRegistrationAllowlist indicates an expected call of UpsertRepositoryNodeRegistrationAllowlist. +func (mr *MockRepositoryServiceServiceMockRecorder) UpsertRepositoryNodeRegistrationAllowlist(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertRepositoryNodeRegistrationAllowlist", reflect.TypeOf((*MockRepositoryServiceService)(nil).UpsertRepositoryNodeRegistrationAllowlist), ctx, req) +} + +// UpsertRepositorySyncSchedule mocks base method. +func (m *MockRepositoryServiceService) UpsertRepositorySyncSchedule(ctx context.Context, req *UpsertRepositorySyncScheduleRequest) (*RepositorySyncScheduleResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpsertRepositorySyncSchedule", ctx, req) + ret0, _ := ret[0].(*RepositorySyncScheduleResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpsertRepositorySyncSchedule indicates an expected call of UpsertRepositorySyncSchedule. +func (mr *MockRepositoryServiceServiceMockRecorder) UpsertRepositorySyncSchedule(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertRepositorySyncSchedule", reflect.TypeOf((*MockRepositoryServiceService)(nil).UpsertRepositorySyncSchedule), ctx, req) +} + +// MockRepositoryServiceClientProxy is a mock of RepositoryServiceClientProxy interface. +type MockRepositoryServiceClientProxy struct { + ctrl *gomock.Controller + recorder *MockRepositoryServiceClientProxyMockRecorder +} + +// MockRepositoryServiceClientProxyMockRecorder is the mock recorder for MockRepositoryServiceClientProxy. +type MockRepositoryServiceClientProxyMockRecorder struct { + mock *MockRepositoryServiceClientProxy +} + +// NewMockRepositoryServiceClientProxy creates a new mock instance. +func NewMockRepositoryServiceClientProxy(ctrl *gomock.Controller) *MockRepositoryServiceClientProxy { + mock := &MockRepositoryServiceClientProxy{ctrl: ctrl} + mock.recorder = &MockRepositoryServiceClientProxyMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepositoryServiceClientProxy) EXPECT() *MockRepositoryServiceClientProxyMockRecorder { + return m.recorder +} + +// AbortRepositoryUpload mocks base method. +func (m *MockRepositoryServiceClientProxy) AbortRepositoryUpload(ctx context.Context, req *AbortRepositoryUploadRequest, opts ...client.Option) (*RepositoryUploadResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "AbortRepositoryUpload", varargs...) + ret0, _ := ret[0].(*RepositoryUploadResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AbortRepositoryUpload indicates an expected call of AbortRepositoryUpload. +func (mr *MockRepositoryServiceClientProxyMockRecorder) AbortRepositoryUpload(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AbortRepositoryUpload", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).AbortRepositoryUpload), varargs...) +} + +// AppendRepositoryOCAIFeedback mocks base method. +func (m *MockRepositoryServiceClientProxy) AppendRepositoryOCAIFeedback(ctx context.Context, req *AppendRepositoryOCAIFeedbackRequest, opts ...client.Option) (*RepositoryOCAIAnalysisResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "AppendRepositoryOCAIFeedback", varargs...) + ret0, _ := ret[0].(*RepositoryOCAIAnalysisResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AppendRepositoryOCAIFeedback indicates an expected call of AppendRepositoryOCAIFeedback. +func (mr *MockRepositoryServiceClientProxyMockRecorder) AppendRepositoryOCAIFeedback(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendRepositoryOCAIFeedback", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).AppendRepositoryOCAIFeedback), varargs...) +} + +// AppendRepositoryUploadChunk mocks base method. +func (m *MockRepositoryServiceClientProxy) AppendRepositoryUploadChunk(ctx context.Context, req *AppendRepositoryUploadChunkRequest, opts ...client.Option) (*RepositoryUploadResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "AppendRepositoryUploadChunk", varargs...) + ret0, _ := ret[0].(*RepositoryUploadResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AppendRepositoryUploadChunk indicates an expected call of AppendRepositoryUploadChunk. +func (mr *MockRepositoryServiceClientProxyMockRecorder) AppendRepositoryUploadChunk(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendRepositoryUploadChunk", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).AppendRepositoryUploadChunk), varargs...) +} + +// BatchApplyRepositories mocks base method. +func (m *MockRepositoryServiceClientProxy) BatchApplyRepositories(ctx context.Context, req *BatchApplyRepositoriesRequest, opts ...client.Option) (*BatchApplyRepositoriesResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "BatchApplyRepositories", varargs...) + ret0, _ := ret[0].(*BatchApplyRepositoriesResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BatchApplyRepositories indicates an expected call of BatchApplyRepositories. +func (mr *MockRepositoryServiceClientProxyMockRecorder) BatchApplyRepositories(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchApplyRepositories", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).BatchApplyRepositories), varargs...) +} + +// BeginRepositoryUpload mocks base method. +func (m *MockRepositoryServiceClientProxy) BeginRepositoryUpload(ctx context.Context, req *BeginRepositoryUploadRequest, opts ...client.Option) (*RepositoryUploadResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "BeginRepositoryUpload", varargs...) + ret0, _ := ret[0].(*RepositoryUploadResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BeginRepositoryUpload indicates an expected call of BeginRepositoryUpload. +func (mr *MockRepositoryServiceClientProxyMockRecorder) BeginRepositoryUpload(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BeginRepositoryUpload", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).BeginRepositoryUpload), varargs...) +} + +// BuildRepositoryOCAIContext mocks base method. +func (m *MockRepositoryServiceClientProxy) BuildRepositoryOCAIContext(ctx context.Context, req *BuildRepositoryOCAIContextRequest, opts ...client.Option) (*RepositoryOCAIContextResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "BuildRepositoryOCAIContext", varargs...) + ret0, _ := ret[0].(*RepositoryOCAIContextResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BuildRepositoryOCAIContext indicates an expected call of BuildRepositoryOCAIContext. +func (mr *MockRepositoryServiceClientProxyMockRecorder) BuildRepositoryOCAIContext(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildRepositoryOCAIContext", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).BuildRepositoryOCAIContext), varargs...) +} + +// CancelRepositoryTask mocks base method. +func (m *MockRepositoryServiceClientProxy) CancelRepositoryTask(ctx context.Context, req *CancelRepositoryTaskRequest, opts ...client.Option) (*RepositoryTaskResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CancelRepositoryTask", varargs...) + ret0, _ := ret[0].(*RepositoryTaskResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CancelRepositoryTask indicates an expected call of CancelRepositoryTask. +func (mr *MockRepositoryServiceClientProxyMockRecorder) CancelRepositoryTask(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CancelRepositoryTask", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).CancelRepositoryTask), varargs...) +} + +// CompleteRepositoryUpload mocks base method. +func (m *MockRepositoryServiceClientProxy) CompleteRepositoryUpload(ctx context.Context, req *CompleteRepositoryUploadRequest, opts ...client.Option) (*CompleteRepositoryUploadResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CompleteRepositoryUpload", varargs...) + ret0, _ := ret[0].(*CompleteRepositoryUploadResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CompleteRepositoryUpload indicates an expected call of CompleteRepositoryUpload. +func (mr *MockRepositoryServiceClientProxyMockRecorder) CompleteRepositoryUpload(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CompleteRepositoryUpload", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).CompleteRepositoryUpload), varargs...) +} + +// ConfigureRepositoryNodeInstallSource mocks base method. +func (m *MockRepositoryServiceClientProxy) ConfigureRepositoryNodeInstallSource(ctx context.Context, req *ConfigureRepositoryNodeInstallSourceRequest, opts ...client.Option) (*RepositoryNodeInstallSourceResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ConfigureRepositoryNodeInstallSource", varargs...) + ret0, _ := ret[0].(*RepositoryNodeInstallSourceResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ConfigureRepositoryNodeInstallSource indicates an expected call of ConfigureRepositoryNodeInstallSource. +func (mr *MockRepositoryServiceClientProxyMockRecorder) ConfigureRepositoryNodeInstallSource(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigureRepositoryNodeInstallSource", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).ConfigureRepositoryNodeInstallSource), varargs...) +} + +// ConfirmRepositoryInventoryImport mocks base method. +func (m *MockRepositoryServiceClientProxy) ConfirmRepositoryInventoryImport(ctx context.Context, req *ConfirmRepositoryInventoryImportRequest, opts ...client.Option) (*RepositoryInventoryImportResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ConfirmRepositoryInventoryImport", varargs...) + ret0, _ := ret[0].(*RepositoryInventoryImportResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ConfirmRepositoryInventoryImport indicates an expected call of ConfirmRepositoryInventoryImport. +func (mr *MockRepositoryServiceClientProxyMockRecorder) ConfirmRepositoryInventoryImport(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfirmRepositoryInventoryImport", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).ConfirmRepositoryInventoryImport), varargs...) +} + +// ConfirmRepositoryPackageDeletion mocks base method. +func (m *MockRepositoryServiceClientProxy) ConfirmRepositoryPackageDeletion(ctx context.Context, req *ConfirmRepositoryPackageDeletionRequest, opts ...client.Option) (*RepositoryTaskResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ConfirmRepositoryPackageDeletion", varargs...) + ret0, _ := ret[0].(*RepositoryTaskResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ConfirmRepositoryPackageDeletion indicates an expected call of ConfirmRepositoryPackageDeletion. +func (mr *MockRepositoryServiceClientProxyMockRecorder) ConfirmRepositoryPackageDeletion(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfirmRepositoryPackageDeletion", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).ConfirmRepositoryPackageDeletion), varargs...) +} + +// ConfirmRepositoryPackageInstall mocks base method. +func (m *MockRepositoryServiceClientProxy) ConfirmRepositoryPackageInstall(ctx context.Context, req *ConfirmRepositoryPackageInstallRequest, opts ...client.Option) (*RepositoryPackageInstallResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ConfirmRepositoryPackageInstall", varargs...) + ret0, _ := ret[0].(*RepositoryPackageInstallResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ConfirmRepositoryPackageInstall indicates an expected call of ConfirmRepositoryPackageInstall. +func (mr *MockRepositoryServiceClientProxyMockRecorder) ConfirmRepositoryPackageInstall(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfirmRepositoryPackageInstall", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).ConfirmRepositoryPackageInstall), varargs...) +} + +// CreateRepository mocks base method. +func (m *MockRepositoryServiceClientProxy) CreateRepository(ctx context.Context, req *CreateRepositoryRequest, opts ...client.Option) (*RepositoryResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CreateRepository", varargs...) + ret0, _ := ret[0].(*RepositoryResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateRepository indicates an expected call of CreateRepository. +func (mr *MockRepositoryServiceClientProxyMockRecorder) CreateRepository(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRepository", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).CreateRepository), varargs...) +} + +// CreateRepositoryDistribution mocks base method. +func (m *MockRepositoryServiceClientProxy) CreateRepositoryDistribution(ctx context.Context, req *CreateRepositoryDistributionRequest, opts ...client.Option) (*RepositoryDistributionResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CreateRepositoryDistribution", varargs...) + ret0, _ := ret[0].(*RepositoryDistributionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateRepositoryDistribution indicates an expected call of CreateRepositoryDistribution. +func (mr *MockRepositoryServiceClientProxyMockRecorder) CreateRepositoryDistribution(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRepositoryDistribution", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).CreateRepositoryDistribution), varargs...) +} + +// CreateRepositoryNodeEnrollment mocks base method. +func (m *MockRepositoryServiceClientProxy) CreateRepositoryNodeEnrollment(ctx context.Context, req *CreateRepositoryNodeEnrollmentRequest, opts ...client.Option) (*RepositoryNodeEnrollmentResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CreateRepositoryNodeEnrollment", varargs...) + ret0, _ := ret[0].(*RepositoryNodeEnrollmentResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateRepositoryNodeEnrollment indicates an expected call of CreateRepositoryNodeEnrollment. +func (mr *MockRepositoryServiceClientProxyMockRecorder) CreateRepositoryNodeEnrollment(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRepositoryNodeEnrollment", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).CreateRepositoryNodeEnrollment), varargs...) +} + +// CreateRepositoryOCAIAnalysis mocks base method. +func (m *MockRepositoryServiceClientProxy) CreateRepositoryOCAIAnalysis(ctx context.Context, req *CreateRepositoryOCAIAnalysisRequest, opts ...client.Option) (*RepositoryOCAIAnalysisResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CreateRepositoryOCAIAnalysis", varargs...) + ret0, _ := ret[0].(*RepositoryOCAIAnalysisResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateRepositoryOCAIAnalysis indicates an expected call of CreateRepositoryOCAIAnalysis. +func (mr *MockRepositoryServiceClientProxyMockRecorder) CreateRepositoryOCAIAnalysis(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRepositoryOCAIAnalysis", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).CreateRepositoryOCAIAnalysis), varargs...) +} + +// CreateRepositoryStorageNode mocks base method. +func (m *MockRepositoryServiceClientProxy) CreateRepositoryStorageNode(ctx context.Context, req *CreateRepositoryStorageNodeRequest, opts ...client.Option) (*RepositoryStorageNodeResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CreateRepositoryStorageNode", varargs...) + ret0, _ := ret[0].(*RepositoryStorageNodeResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateRepositoryStorageNode indicates an expected call of CreateRepositoryStorageNode. +func (mr *MockRepositoryServiceClientProxyMockRecorder) CreateRepositoryStorageNode(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRepositoryStorageNode", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).CreateRepositoryStorageNode), varargs...) +} + +// CreateRepositoryUpstreamProfile mocks base method. +func (m *MockRepositoryServiceClientProxy) CreateRepositoryUpstreamProfile(ctx context.Context, req *CreateRepositoryUpstreamProfileRequest, opts ...client.Option) (*RepositoryUpstreamProfileResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CreateRepositoryUpstreamProfile", varargs...) + ret0, _ := ret[0].(*RepositoryUpstreamProfileResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateRepositoryUpstreamProfile indicates an expected call of CreateRepositoryUpstreamProfile. +func (mr *MockRepositoryServiceClientProxyMockRecorder) CreateRepositoryUpstreamProfile(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRepositoryUpstreamProfile", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).CreateRepositoryUpstreamProfile), varargs...) +} + +// DeleteRepository mocks base method. +func (m *MockRepositoryServiceClientProxy) DeleteRepository(ctx context.Context, req *DeleteRepositoryRequest, opts ...client.Option) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DeleteRepository", varargs...) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteRepository indicates an expected call of DeleteRepository. +func (mr *MockRepositoryServiceClientProxyMockRecorder) DeleteRepository(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRepository", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).DeleteRepository), varargs...) +} + +// DeleteRepositoryGPGKey mocks base method. +func (m *MockRepositoryServiceClientProxy) DeleteRepositoryGPGKey(ctx context.Context, req *DeleteRepositoryGPGKeyRequest, opts ...client.Option) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DeleteRepositoryGPGKey", varargs...) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteRepositoryGPGKey indicates an expected call of DeleteRepositoryGPGKey. +func (mr *MockRepositoryServiceClientProxyMockRecorder) DeleteRepositoryGPGKey(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRepositoryGPGKey", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).DeleteRepositoryGPGKey), varargs...) +} + +// DeleteRepositoryPackage mocks base method. +func (m *MockRepositoryServiceClientProxy) DeleteRepositoryPackage(ctx context.Context, req *DeleteRepositoryPackageRequest, opts ...client.Option) (*RepositoryTaskResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DeleteRepositoryPackage", varargs...) + ret0, _ := ret[0].(*RepositoryTaskResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteRepositoryPackage indicates an expected call of DeleteRepositoryPackage. +func (mr *MockRepositoryServiceClientProxyMockRecorder) DeleteRepositoryPackage(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRepositoryPackage", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).DeleteRepositoryPackage), varargs...) +} + +// DeleteRepositoryStorageNode mocks base method. +func (m *MockRepositoryServiceClientProxy) DeleteRepositoryStorageNode(ctx context.Context, req *DeleteRepositoryStorageNodeRequest, opts ...client.Option) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DeleteRepositoryStorageNode", varargs...) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteRepositoryStorageNode indicates an expected call of DeleteRepositoryStorageNode. +func (mr *MockRepositoryServiceClientProxyMockRecorder) DeleteRepositoryStorageNode(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRepositoryStorageNode", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).DeleteRepositoryStorageNode), varargs...) +} + +// DeleteRepositoryUpstreamProfile mocks base method. +func (m *MockRepositoryServiceClientProxy) DeleteRepositoryUpstreamProfile(ctx context.Context, req *DeleteRepositoryUpstreamProfileRequest, opts ...client.Option) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DeleteRepositoryUpstreamProfile", varargs...) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteRepositoryUpstreamProfile indicates an expected call of DeleteRepositoryUpstreamProfile. +func (mr *MockRepositoryServiceClientProxyMockRecorder) DeleteRepositoryUpstreamProfile(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRepositoryUpstreamProfile", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).DeleteRepositoryUpstreamProfile), varargs...) +} + +// DescribeRepository mocks base method. +func (m *MockRepositoryServiceClientProxy) DescribeRepository(ctx context.Context, req *DescribeRepositoryRequest, opts ...client.Option) (*RepositoryResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DescribeRepository", varargs...) + ret0, _ := ret[0].(*RepositoryResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeRepository indicates an expected call of DescribeRepository. +func (mr *MockRepositoryServiceClientProxyMockRecorder) DescribeRepository(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeRepository", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).DescribeRepository), varargs...) +} + +// DescribeRepositoryDistribution mocks base method. +func (m *MockRepositoryServiceClientProxy) DescribeRepositoryDistribution(ctx context.Context, req *DescribeRepositoryDistributionRequest, opts ...client.Option) (*RepositoryDistributionResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DescribeRepositoryDistribution", varargs...) + ret0, _ := ret[0].(*RepositoryDistributionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeRepositoryDistribution indicates an expected call of DescribeRepositoryDistribution. +func (mr *MockRepositoryServiceClientProxyMockRecorder) DescribeRepositoryDistribution(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeRepositoryDistribution", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).DescribeRepositoryDistribution), varargs...) +} + +// DescribeRepositoryOCAIAnalysis mocks base method. +func (m *MockRepositoryServiceClientProxy) DescribeRepositoryOCAIAnalysis(ctx context.Context, req *DescribeRepositoryOCAIAnalysisRequest, opts ...client.Option) (*RepositoryOCAIAnalysisResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DescribeRepositoryOCAIAnalysis", varargs...) + ret0, _ := ret[0].(*RepositoryOCAIAnalysisResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeRepositoryOCAIAnalysis indicates an expected call of DescribeRepositoryOCAIAnalysis. +func (mr *MockRepositoryServiceClientProxyMockRecorder) DescribeRepositoryOCAIAnalysis(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeRepositoryOCAIAnalysis", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).DescribeRepositoryOCAIAnalysis), varargs...) +} + +// DescribeRepositoryPackageInstall mocks base method. +func (m *MockRepositoryServiceClientProxy) DescribeRepositoryPackageInstall(ctx context.Context, req *DescribeRepositoryPackageInstallRequest, opts ...client.Option) (*RepositoryPackageInstallResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DescribeRepositoryPackageInstall", varargs...) + ret0, _ := ret[0].(*RepositoryPackageInstallResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeRepositoryPackageInstall indicates an expected call of DescribeRepositoryPackageInstall. +func (mr *MockRepositoryServiceClientProxyMockRecorder) DescribeRepositoryPackageInstall(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeRepositoryPackageInstall", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).DescribeRepositoryPackageInstall), varargs...) +} + +// DescribeRepositoryStorageNode mocks base method. +func (m *MockRepositoryServiceClientProxy) DescribeRepositoryStorageNode(ctx context.Context, req *DescribeRepositoryStorageNodeRequest, opts ...client.Option) (*RepositoryStorageNodeResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DescribeRepositoryStorageNode", varargs...) + ret0, _ := ret[0].(*RepositoryStorageNodeResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeRepositoryStorageNode indicates an expected call of DescribeRepositoryStorageNode. +func (mr *MockRepositoryServiceClientProxyMockRecorder) DescribeRepositoryStorageNode(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeRepositoryStorageNode", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).DescribeRepositoryStorageNode), varargs...) +} + +// DescribeRepositorySyncSchedule mocks base method. +func (m *MockRepositoryServiceClientProxy) DescribeRepositorySyncSchedule(ctx context.Context, req *DescribeRepositorySyncScheduleRequest, opts ...client.Option) (*RepositorySyncScheduleResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DescribeRepositorySyncSchedule", varargs...) + ret0, _ := ret[0].(*RepositorySyncScheduleResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeRepositorySyncSchedule indicates an expected call of DescribeRepositorySyncSchedule. +func (mr *MockRepositoryServiceClientProxyMockRecorder) DescribeRepositorySyncSchedule(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeRepositorySyncSchedule", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).DescribeRepositorySyncSchedule), varargs...) +} + +// DescribeRepositoryTask mocks base method. +func (m *MockRepositoryServiceClientProxy) DescribeRepositoryTask(ctx context.Context, req *DescribeRepositoryTaskRequest, opts ...client.Option) (*RepositoryTaskResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DescribeRepositoryTask", varargs...) + ret0, _ := ret[0].(*RepositoryTaskResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeRepositoryTask indicates an expected call of DescribeRepositoryTask. +func (mr *MockRepositoryServiceClientProxyMockRecorder) DescribeRepositoryTask(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeRepositoryTask", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).DescribeRepositoryTask), varargs...) +} + +// DescribeRepositoryTaskLog mocks base method. +func (m *MockRepositoryServiceClientProxy) DescribeRepositoryTaskLog(ctx context.Context, req *DescribeRepositoryTaskLogRequest, opts ...client.Option) (*RepositoryTaskLogResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DescribeRepositoryTaskLog", varargs...) + ret0, _ := ret[0].(*RepositoryTaskLogResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeRepositoryTaskLog indicates an expected call of DescribeRepositoryTaskLog. +func (mr *MockRepositoryServiceClientProxyMockRecorder) DescribeRepositoryTaskLog(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeRepositoryTaskLog", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).DescribeRepositoryTaskLog), varargs...) +} + +// DescribeRepositoryUpload mocks base method. +func (m *MockRepositoryServiceClientProxy) DescribeRepositoryUpload(ctx context.Context, req *DescribeRepositoryUploadRequest, opts ...client.Option) (*RepositoryUploadResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DescribeRepositoryUpload", varargs...) + ret0, _ := ret[0].(*RepositoryUploadResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeRepositoryUpload indicates an expected call of DescribeRepositoryUpload. +func (mr *MockRepositoryServiceClientProxyMockRecorder) DescribeRepositoryUpload(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeRepositoryUpload", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).DescribeRepositoryUpload), varargs...) +} + +// DescribeRepositoryUploadChunks mocks base method. +func (m *MockRepositoryServiceClientProxy) DescribeRepositoryUploadChunks(ctx context.Context, req *DescribeRepositoryUploadRequest, opts ...client.Option) (*DescribeRepositoryUploadChunksResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DescribeRepositoryUploadChunks", varargs...) + ret0, _ := ret[0].(*DescribeRepositoryUploadChunksResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeRepositoryUploadChunks indicates an expected call of DescribeRepositoryUploadChunks. +func (mr *MockRepositoryServiceClientProxyMockRecorder) DescribeRepositoryUploadChunks(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeRepositoryUploadChunks", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).DescribeRepositoryUploadChunks), varargs...) +} + +// DescribeRepositoryUpstreamProfile mocks base method. +func (m *MockRepositoryServiceClientProxy) DescribeRepositoryUpstreamProfile(ctx context.Context, req *DescribeRepositoryUpstreamProfileRequest, opts ...client.Option) (*RepositoryUpstreamProfileResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DescribeRepositoryUpstreamProfile", varargs...) + ret0, _ := ret[0].(*RepositoryUpstreamProfileResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeRepositoryUpstreamProfile indicates an expected call of DescribeRepositoryUpstreamProfile. +func (mr *MockRepositoryServiceClientProxyMockRecorder) DescribeRepositoryUpstreamProfile(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeRepositoryUpstreamProfile", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).DescribeRepositoryUpstreamProfile), varargs...) +} + +// DownloadRepositoryNodeArtifact mocks base method. +func (m *MockRepositoryServiceClientProxy) DownloadRepositoryNodeArtifact(ctx context.Context, req *DownloadRepositoryNodeArtifactRequest, opts ...client.Option) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DownloadRepositoryNodeArtifact", varargs...) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DownloadRepositoryNodeArtifact indicates an expected call of DownloadRepositoryNodeArtifact. +func (mr *MockRepositoryServiceClientProxyMockRecorder) DownloadRepositoryNodeArtifact(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DownloadRepositoryNodeArtifact", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).DownloadRepositoryNodeArtifact), varargs...) +} + +// DownloadRepositoryTaskLog mocks base method. +func (m *MockRepositoryServiceClientProxy) DownloadRepositoryTaskLog(ctx context.Context, req *DownloadRepositoryTaskLogRequest, opts ...client.Option) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DownloadRepositoryTaskLog", varargs...) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DownloadRepositoryTaskLog indicates an expected call of DownloadRepositoryTaskLog. +func (mr *MockRepositoryServiceClientProxyMockRecorder) DownloadRepositoryTaskLog(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DownloadRepositoryTaskLog", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).DownloadRepositoryTaskLog), varargs...) +} + +// ExportRepositoryDistributionResult mocks base method. +func (m *MockRepositoryServiceClientProxy) ExportRepositoryDistributionResult(ctx context.Context, req *ExportRepositoryDistributionResultRequest, opts ...client.Option) (*RepositoryExportResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ExportRepositoryDistributionResult", varargs...) + ret0, _ := ret[0].(*RepositoryExportResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExportRepositoryDistributionResult indicates an expected call of ExportRepositoryDistributionResult. +func (mr *MockRepositoryServiceClientProxyMockRecorder) ExportRepositoryDistributionResult(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExportRepositoryDistributionResult", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).ExportRepositoryDistributionResult), varargs...) +} + +// ExportRepositoryInventory mocks base method. +func (m *MockRepositoryServiceClientProxy) ExportRepositoryInventory(ctx context.Context, req *ExportRepositoryInventoryRequest, opts ...client.Option) (*RepositoryExportResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ExportRepositoryInventory", varargs...) + ret0, _ := ret[0].(*RepositoryExportResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExportRepositoryInventory indicates an expected call of ExportRepositoryInventory. +func (mr *MockRepositoryServiceClientProxyMockRecorder) ExportRepositoryInventory(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExportRepositoryInventory", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).ExportRepositoryInventory), varargs...) +} + +// ExportRepositoryTaskDiagnosis mocks base method. +func (m *MockRepositoryServiceClientProxy) ExportRepositoryTaskDiagnosis(ctx context.Context, req *ExportRepositoryTaskDiagnosisRequest, opts ...client.Option) (*RepositoryDiagnosisReportResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ExportRepositoryTaskDiagnosis", varargs...) + ret0, _ := ret[0].(*RepositoryDiagnosisReportResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExportRepositoryTaskDiagnosis indicates an expected call of ExportRepositoryTaskDiagnosis. +func (mr *MockRepositoryServiceClientProxyMockRecorder) ExportRepositoryTaskDiagnosis(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExportRepositoryTaskDiagnosis", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).ExportRepositoryTaskDiagnosis), varargs...) +} + +// ExportRepositoryValidationReport mocks base method. +func (m *MockRepositoryServiceClientProxy) ExportRepositoryValidationReport(ctx context.Context, req *ExportRepositoryValidationReportRequest, opts ...client.Option) (*RepositoryExportResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ExportRepositoryValidationReport", varargs...) + ret0, _ := ret[0].(*RepositoryExportResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExportRepositoryValidationReport indicates an expected call of ExportRepositoryValidationReport. +func (mr *MockRepositoryServiceClientProxyMockRecorder) ExportRepositoryValidationReport(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExportRepositoryValidationReport", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).ExportRepositoryValidationReport), varargs...) +} + +// GenerateRepositoryConfig mocks base method. +func (m *MockRepositoryServiceClientProxy) GenerateRepositoryConfig(ctx context.Context, req *GenerateRepositoryConfigRequest, opts ...client.Option) (*RepositoryConfigResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GenerateRepositoryConfig", varargs...) + ret0, _ := ret[0].(*RepositoryConfigResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GenerateRepositoryConfig indicates an expected call of GenerateRepositoryConfig. +func (mr *MockRepositoryServiceClientProxyMockRecorder) GenerateRepositoryConfig(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateRepositoryConfig", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).GenerateRepositoryConfig), varargs...) +} + +// ListRepositories mocks base method. +func (m *MockRepositoryServiceClientProxy) ListRepositories(ctx context.Context, req *ListRepositoriesRequest, opts ...client.Option) (*ListRepositoriesResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ListRepositories", varargs...) + ret0, _ := ret[0].(*ListRepositoriesResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRepositories indicates an expected call of ListRepositories. +func (mr *MockRepositoryServiceClientProxyMockRecorder) ListRepositories(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRepositories", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).ListRepositories), varargs...) +} + +// ListRepositoryDistributionNodes mocks base method. +func (m *MockRepositoryServiceClientProxy) ListRepositoryDistributionNodes(ctx context.Context, req *ListRepositoryDistributionNodesRequest, opts ...client.Option) (*ListRepositoryDistributionNodesResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ListRepositoryDistributionNodes", varargs...) + ret0, _ := ret[0].(*ListRepositoryDistributionNodesResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRepositoryDistributionNodes indicates an expected call of ListRepositoryDistributionNodes. +func (mr *MockRepositoryServiceClientProxyMockRecorder) ListRepositoryDistributionNodes(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRepositoryDistributionNodes", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).ListRepositoryDistributionNodes), varargs...) +} + +// ListRepositoryDistributions mocks base method. +func (m *MockRepositoryServiceClientProxy) ListRepositoryDistributions(ctx context.Context, req *ListRepositoryDistributionsRequest, opts ...client.Option) (*ListRepositoryDistributionsResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ListRepositoryDistributions", varargs...) + ret0, _ := ret[0].(*ListRepositoryDistributionsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRepositoryDistributions indicates an expected call of ListRepositoryDistributions. +func (mr *MockRepositoryServiceClientProxyMockRecorder) ListRepositoryDistributions(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRepositoryDistributions", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).ListRepositoryDistributions), varargs...) +} + +// ListRepositoryGPGKeys mocks base method. +func (m *MockRepositoryServiceClientProxy) ListRepositoryGPGKeys(ctx context.Context, req *ListRepositoryGPGKeysRequest, opts ...client.Option) (*ListRepositoryGPGKeysResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ListRepositoryGPGKeys", varargs...) + ret0, _ := ret[0].(*ListRepositoryGPGKeysResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRepositoryGPGKeys indicates an expected call of ListRepositoryGPGKeys. +func (mr *MockRepositoryServiceClientProxyMockRecorder) ListRepositoryGPGKeys(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRepositoryGPGKeys", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).ListRepositoryGPGKeys), varargs...) +} + +// ListRepositoryNodeArtifacts mocks base method. +func (m *MockRepositoryServiceClientProxy) ListRepositoryNodeArtifacts(ctx context.Context, req *ListRepositoryNodeArtifactsRequest, opts ...client.Option) (*ListRepositoryNodeArtifactsResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ListRepositoryNodeArtifacts", varargs...) + ret0, _ := ret[0].(*ListRepositoryNodeArtifactsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRepositoryNodeArtifacts indicates an expected call of ListRepositoryNodeArtifacts. +func (mr *MockRepositoryServiceClientProxyMockRecorder) ListRepositoryNodeArtifacts(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRepositoryNodeArtifacts", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).ListRepositoryNodeArtifacts), varargs...) +} + +// ListRepositoryNodeInstallSources mocks base method. +func (m *MockRepositoryServiceClientProxy) ListRepositoryNodeInstallSources(ctx context.Context, req *ListRepositoryNodeInstallSourcesRequest, opts ...client.Option) (*ListRepositoryNodeInstallSourcesResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ListRepositoryNodeInstallSources", varargs...) + ret0, _ := ret[0].(*ListRepositoryNodeInstallSourcesResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRepositoryNodeInstallSources indicates an expected call of ListRepositoryNodeInstallSources. +func (mr *MockRepositoryServiceClientProxyMockRecorder) ListRepositoryNodeInstallSources(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRepositoryNodeInstallSources", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).ListRepositoryNodeInstallSources), varargs...) +} + +// ListRepositoryNodeRegistrationAllowlists mocks base method. +func (m *MockRepositoryServiceClientProxy) ListRepositoryNodeRegistrationAllowlists(ctx context.Context, req *ListRepositoryNodeRegistrationAllowlistsRequest, opts ...client.Option) (*ListRepositoryNodeRegistrationAllowlistsResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ListRepositoryNodeRegistrationAllowlists", varargs...) + ret0, _ := ret[0].(*ListRepositoryNodeRegistrationAllowlistsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRepositoryNodeRegistrationAllowlists indicates an expected call of ListRepositoryNodeRegistrationAllowlists. +func (mr *MockRepositoryServiceClientProxyMockRecorder) ListRepositoryNodeRegistrationAllowlists(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRepositoryNodeRegistrationAllowlists", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).ListRepositoryNodeRegistrationAllowlists), varargs...) +} + +// ListRepositoryPackages mocks base method. +func (m *MockRepositoryServiceClientProxy) ListRepositoryPackages(ctx context.Context, req *ListRepositoryPackagesRequest, opts ...client.Option) (*ListRepositoryPackagesResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ListRepositoryPackages", varargs...) + ret0, _ := ret[0].(*ListRepositoryPackagesResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRepositoryPackages indicates an expected call of ListRepositoryPackages. +func (mr *MockRepositoryServiceClientProxyMockRecorder) ListRepositoryPackages(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRepositoryPackages", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).ListRepositoryPackages), varargs...) +} + +// ListRepositoryPreflightRecords mocks base method. +func (m *MockRepositoryServiceClientProxy) ListRepositoryPreflightRecords(ctx context.Context, req *ListRepositoryPreflightRecordsRequest, opts ...client.Option) (*ListRepositoryPreflightRecordsResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ListRepositoryPreflightRecords", varargs...) + ret0, _ := ret[0].(*ListRepositoryPreflightRecordsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRepositoryPreflightRecords indicates an expected call of ListRepositoryPreflightRecords. +func (mr *MockRepositoryServiceClientProxyMockRecorder) ListRepositoryPreflightRecords(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRepositoryPreflightRecords", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).ListRepositoryPreflightRecords), varargs...) +} + +// ListRepositoryQuarantine mocks base method. +func (m *MockRepositoryServiceClientProxy) ListRepositoryQuarantine(ctx context.Context, req *ListRepositoryQuarantineRequest, opts ...client.Option) (*ListRepositoryQuarantineResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ListRepositoryQuarantine", varargs...) + ret0, _ := ret[0].(*ListRepositoryQuarantineResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRepositoryQuarantine indicates an expected call of ListRepositoryQuarantine. +func (mr *MockRepositoryServiceClientProxyMockRecorder) ListRepositoryQuarantine(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRepositoryQuarantine", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).ListRepositoryQuarantine), varargs...) +} + +// ListRepositoryStorageNodes mocks base method. +func (m *MockRepositoryServiceClientProxy) ListRepositoryStorageNodes(ctx context.Context, req *ListRepositoryStorageNodesRequest, opts ...client.Option) (*ListRepositoryStorageNodesResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ListRepositoryStorageNodes", varargs...) + ret0, _ := ret[0].(*ListRepositoryStorageNodesResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRepositoryStorageNodes indicates an expected call of ListRepositoryStorageNodes. +func (mr *MockRepositoryServiceClientProxyMockRecorder) ListRepositoryStorageNodes(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRepositoryStorageNodes", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).ListRepositoryStorageNodes), varargs...) +} + +// ListRepositoryTasks mocks base method. +func (m *MockRepositoryServiceClientProxy) ListRepositoryTasks(ctx context.Context, req *ListRepositoryTasksRequest, opts ...client.Option) (*ListRepositoryTasksResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ListRepositoryTasks", varargs...) + ret0, _ := ret[0].(*ListRepositoryTasksResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRepositoryTasks indicates an expected call of ListRepositoryTasks. +func (mr *MockRepositoryServiceClientProxyMockRecorder) ListRepositoryTasks(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRepositoryTasks", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).ListRepositoryTasks), varargs...) +} + +// ListRepositoryUpstreamProfiles mocks base method. +func (m *MockRepositoryServiceClientProxy) ListRepositoryUpstreamProfiles(ctx context.Context, req *ListRepositoryUpstreamProfilesRequest, opts ...client.Option) (*ListRepositoryUpstreamProfilesResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ListRepositoryUpstreamProfiles", varargs...) + ret0, _ := ret[0].(*ListRepositoryUpstreamProfilesResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRepositoryUpstreamProfiles indicates an expected call of ListRepositoryUpstreamProfiles. +func (mr *MockRepositoryServiceClientProxyMockRecorder) ListRepositoryUpstreamProfiles(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRepositoryUpstreamProfiles", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).ListRepositoryUpstreamProfiles), varargs...) +} + +// MigrateRepository mocks base method. +func (m *MockRepositoryServiceClientProxy) MigrateRepository(ctx context.Context, req *MigrateRepositoryRequest, opts ...client.Option) (*RepositoryTaskResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "MigrateRepository", varargs...) + ret0, _ := ret[0].(*RepositoryTaskResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// MigrateRepository indicates an expected call of MigrateRepository. +func (mr *MockRepositoryServiceClientProxyMockRecorder) MigrateRepository(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MigrateRepository", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).MigrateRepository), varargs...) +} + +// PauseRepositorySyncSchedule mocks base method. +func (m *MockRepositoryServiceClientProxy) PauseRepositorySyncSchedule(ctx context.Context, req *ChangeRepositorySyncScheduleStatusRequest, opts ...client.Option) (*RepositorySyncScheduleResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "PauseRepositorySyncSchedule", varargs...) + ret0, _ := ret[0].(*RepositorySyncScheduleResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PauseRepositorySyncSchedule indicates an expected call of PauseRepositorySyncSchedule. +func (mr *MockRepositoryServiceClientProxyMockRecorder) PauseRepositorySyncSchedule(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PauseRepositorySyncSchedule", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).PauseRepositorySyncSchedule), varargs...) +} + +// PauseRepositoryUpload mocks base method. +func (m *MockRepositoryServiceClientProxy) PauseRepositoryUpload(ctx context.Context, req *DescribeRepositoryUploadRequest, opts ...client.Option) (*RepositoryUploadResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "PauseRepositoryUpload", varargs...) + ret0, _ := ret[0].(*RepositoryUploadResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PauseRepositoryUpload indicates an expected call of PauseRepositoryUpload. +func (mr *MockRepositoryServiceClientProxyMockRecorder) PauseRepositoryUpload(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PauseRepositoryUpload", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).PauseRepositoryUpload), varargs...) +} + +// PreviewRepositoryInventoryImport mocks base method. +func (m *MockRepositoryServiceClientProxy) PreviewRepositoryInventoryImport(ctx context.Context, req *PreviewRepositoryInventoryImportRequest, opts ...client.Option) (*RepositoryInventoryImportPreviewResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "PreviewRepositoryInventoryImport", varargs...) + ret0, _ := ret[0].(*RepositoryInventoryImportPreviewResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PreviewRepositoryInventoryImport indicates an expected call of PreviewRepositoryInventoryImport. +func (mr *MockRepositoryServiceClientProxyMockRecorder) PreviewRepositoryInventoryImport(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PreviewRepositoryInventoryImport", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).PreviewRepositoryInventoryImport), varargs...) +} + +// PreviewRepositoryPackageDeletion mocks base method. +func (m *MockRepositoryServiceClientProxy) PreviewRepositoryPackageDeletion(ctx context.Context, req *PreviewRepositoryPackageDeletionRequest, opts ...client.Option) (*RepositoryPackageDeletionPreviewResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "PreviewRepositoryPackageDeletion", varargs...) + ret0, _ := ret[0].(*RepositoryPackageDeletionPreviewResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PreviewRepositoryPackageDeletion indicates an expected call of PreviewRepositoryPackageDeletion. +func (mr *MockRepositoryServiceClientProxyMockRecorder) PreviewRepositoryPackageDeletion(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PreviewRepositoryPackageDeletion", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).PreviewRepositoryPackageDeletion), varargs...) +} + +// PreviewRepositoryPackageInstall mocks base method. +func (m *MockRepositoryServiceClientProxy) PreviewRepositoryPackageInstall(ctx context.Context, req *PreviewRepositoryPackageInstallRequest, opts ...client.Option) (*RepositoryPackageInstallPreviewResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "PreviewRepositoryPackageInstall", varargs...) + ret0, _ := ret[0].(*RepositoryPackageInstallPreviewResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PreviewRepositoryPackageInstall indicates an expected call of PreviewRepositoryPackageInstall. +func (mr *MockRepositoryServiceClientProxyMockRecorder) PreviewRepositoryPackageInstall(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PreviewRepositoryPackageInstall", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).PreviewRepositoryPackageInstall), varargs...) +} + +// ProbeRepositoryStorageNode mocks base method. +func (m *MockRepositoryServiceClientProxy) ProbeRepositoryStorageNode(ctx context.Context, req *ProbeRepositoryStorageNodeRequest, opts ...client.Option) (*RepositoryStorageNodeProbeResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ProbeRepositoryStorageNode", varargs...) + ret0, _ := ret[0].(*RepositoryStorageNodeProbeResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ProbeRepositoryStorageNode indicates an expected call of ProbeRepositoryStorageNode. +func (mr *MockRepositoryServiceClientProxyMockRecorder) ProbeRepositoryStorageNode(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProbeRepositoryStorageNode", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).ProbeRepositoryStorageNode), varargs...) +} + +// PutRepositoryUploadChunk mocks base method. +func (m *MockRepositoryServiceClientProxy) PutRepositoryUploadChunk(ctx context.Context, req *PutRepositoryUploadChunkRequest, opts ...client.Option) (*PutRepositoryUploadChunkResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "PutRepositoryUploadChunk", varargs...) + ret0, _ := ret[0].(*PutRepositoryUploadChunkResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PutRepositoryUploadChunk indicates an expected call of PutRepositoryUploadChunk. +func (mr *MockRepositoryServiceClientProxyMockRecorder) PutRepositoryUploadChunk(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutRepositoryUploadChunk", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).PutRepositoryUploadChunk), varargs...) +} + +// RegisterRepositoryGPGKey mocks base method. +func (m *MockRepositoryServiceClientProxy) RegisterRepositoryGPGKey(ctx context.Context, req *RegisterRepositoryGPGKeyRequest, opts ...client.Option) (*RepositoryGPGKeyResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "RegisterRepositoryGPGKey", varargs...) + ret0, _ := ret[0].(*RepositoryGPGKeyResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RegisterRepositoryGPGKey indicates an expected call of RegisterRepositoryGPGKey. +func (mr *MockRepositoryServiceClientProxyMockRecorder) RegisterRepositoryGPGKey(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterRepositoryGPGKey", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).RegisterRepositoryGPGKey), varargs...) +} + +// RegisterRepositoryNodeArtifact mocks base method. +func (m *MockRepositoryServiceClientProxy) RegisterRepositoryNodeArtifact(ctx context.Context, req *RegisterRepositoryNodeArtifactRequest, opts ...client.Option) (*RepositoryNodeArtifactResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "RegisterRepositoryNodeArtifact", varargs...) + ret0, _ := ret[0].(*RepositoryNodeArtifactResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RegisterRepositoryNodeArtifact indicates an expected call of RegisterRepositoryNodeArtifact. +func (mr *MockRepositoryServiceClientProxyMockRecorder) RegisterRepositoryNodeArtifact(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterRepositoryNodeArtifact", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).RegisterRepositoryNodeArtifact), varargs...) +} + +// ResumeRepositorySyncSchedule mocks base method. +func (m *MockRepositoryServiceClientProxy) ResumeRepositorySyncSchedule(ctx context.Context, req *ChangeRepositorySyncScheduleStatusRequest, opts ...client.Option) (*RepositorySyncScheduleResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ResumeRepositorySyncSchedule", varargs...) + ret0, _ := ret[0].(*RepositorySyncScheduleResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ResumeRepositorySyncSchedule indicates an expected call of ResumeRepositorySyncSchedule. +func (mr *MockRepositoryServiceClientProxyMockRecorder) ResumeRepositorySyncSchedule(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResumeRepositorySyncSchedule", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).ResumeRepositorySyncSchedule), varargs...) +} + +// ResumeRepositoryUpload mocks base method. +func (m *MockRepositoryServiceClientProxy) ResumeRepositoryUpload(ctx context.Context, req *DescribeRepositoryUploadRequest, opts ...client.Option) (*RepositoryUploadResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ResumeRepositoryUpload", varargs...) + ret0, _ := ret[0].(*RepositoryUploadResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ResumeRepositoryUpload indicates an expected call of ResumeRepositoryUpload. +func (mr *MockRepositoryServiceClientProxyMockRecorder) ResumeRepositoryUpload(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResumeRepositoryUpload", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).ResumeRepositoryUpload), varargs...) +} + +// RetryRepositoryTask mocks base method. +func (m *MockRepositoryServiceClientProxy) RetryRepositoryTask(ctx context.Context, req *RetryRepositoryTaskRequest, opts ...client.Option) (*RepositoryTaskResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "RetryRepositoryTask", varargs...) + ret0, _ := ret[0].(*RepositoryTaskResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RetryRepositoryTask indicates an expected call of RetryRepositoryTask. +func (mr *MockRepositoryServiceClientProxyMockRecorder) RetryRepositoryTask(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RetryRepositoryTask", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).RetryRepositoryTask), varargs...) +} + +// SyncRepository mocks base method. +func (m *MockRepositoryServiceClientProxy) SyncRepository(ctx context.Context, req *SyncRepositoryRequest, opts ...client.Option) (*RepositoryTaskResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "SyncRepository", varargs...) + ret0, _ := ret[0].(*RepositoryTaskResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SyncRepository indicates an expected call of SyncRepository. +func (mr *MockRepositoryServiceClientProxyMockRecorder) SyncRepository(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SyncRepository", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).SyncRepository), varargs...) +} + +// UpdateRepository mocks base method. +func (m *MockRepositoryServiceClientProxy) UpdateRepository(ctx context.Context, req *UpdateRepositoryRequest, opts ...client.Option) (*RepositoryResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "UpdateRepository", varargs...) + ret0, _ := ret[0].(*RepositoryResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateRepository indicates an expected call of UpdateRepository. +func (mr *MockRepositoryServiceClientProxyMockRecorder) UpdateRepository(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateRepository", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).UpdateRepository), varargs...) +} + +// UpdateRepositoryStorageNode mocks base method. +func (m *MockRepositoryServiceClientProxy) UpdateRepositoryStorageNode(ctx context.Context, req *UpdateRepositoryStorageNodeRequest, opts ...client.Option) (*RepositoryStorageNodeResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "UpdateRepositoryStorageNode", varargs...) + ret0, _ := ret[0].(*RepositoryStorageNodeResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateRepositoryStorageNode indicates an expected call of UpdateRepositoryStorageNode. +func (mr *MockRepositoryServiceClientProxyMockRecorder) UpdateRepositoryStorageNode(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateRepositoryStorageNode", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).UpdateRepositoryStorageNode), varargs...) +} + +// UpdateRepositoryUpstreamProfile mocks base method. +func (m *MockRepositoryServiceClientProxy) UpdateRepositoryUpstreamProfile(ctx context.Context, req *UpdateRepositoryUpstreamProfileRequest, opts ...client.Option) (*RepositoryUpstreamProfileResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "UpdateRepositoryUpstreamProfile", varargs...) + ret0, _ := ret[0].(*RepositoryUpstreamProfileResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateRepositoryUpstreamProfile indicates an expected call of UpdateRepositoryUpstreamProfile. +func (mr *MockRepositoryServiceClientProxyMockRecorder) UpdateRepositoryUpstreamProfile(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateRepositoryUpstreamProfile", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).UpdateRepositoryUpstreamProfile), varargs...) +} + +// UpsertRepositoryNodeRegistrationAllowlist mocks base method. +func (m *MockRepositoryServiceClientProxy) UpsertRepositoryNodeRegistrationAllowlist(ctx context.Context, req *UpsertRepositoryNodeRegistrationAllowlistRequest, opts ...client.Option) (*RepositoryNodeRegistrationAllowlistResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "UpsertRepositoryNodeRegistrationAllowlist", varargs...) + ret0, _ := ret[0].(*RepositoryNodeRegistrationAllowlistResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpsertRepositoryNodeRegistrationAllowlist indicates an expected call of UpsertRepositoryNodeRegistrationAllowlist. +func (mr *MockRepositoryServiceClientProxyMockRecorder) UpsertRepositoryNodeRegistrationAllowlist(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertRepositoryNodeRegistrationAllowlist", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).UpsertRepositoryNodeRegistrationAllowlist), varargs...) +} + +// UpsertRepositorySyncSchedule mocks base method. +func (m *MockRepositoryServiceClientProxy) UpsertRepositorySyncSchedule(ctx context.Context, req *UpsertRepositorySyncScheduleRequest, opts ...client.Option) (*RepositorySyncScheduleResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "UpsertRepositorySyncSchedule", varargs...) + ret0, _ := ret[0].(*RepositorySyncScheduleResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpsertRepositorySyncSchedule indicates an expected call of UpsertRepositorySyncSchedule. +func (mr *MockRepositoryServiceClientProxyMockRecorder) UpsertRepositorySyncSchedule(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertRepositorySyncSchedule", reflect.TypeOf((*MockRepositoryServiceClientProxy)(nil).UpsertRepositorySyncSchedule), varargs...) +} diff --git a/manager/backend/protocol/repositorynode/Makefile b/manager/backend/protocol/repositorynode/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..f2ee2dbc5e7167900d8a6d19439657876bed017f --- /dev/null +++ b/manager/backend/protocol/repositorynode/Makefile @@ -0,0 +1,4 @@ +.PHONY: build + +build: + bash ./build.sh diff --git a/manager/backend/protocol/repositorynode/apidocs.swagger.json b/manager/backend/protocol/repositorynode/apidocs.swagger.json new file mode 100644 index 0000000000000000000000000000000000000000..2f1243678e3dbcdb00e8ce812e98032e3feb2f33 --- /dev/null +++ b/manager/backend/protocol/repositorynode/apidocs.swagger.json @@ -0,0 +1,1036 @@ +{ + "swagger": "2.0", + "info": { + "title": "repository_node", + "description": "The api document of repository_node.proto", + "version": "2.0" + }, + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/trpc.ocmanager.repositorynode.RepositoryNodeBootstrapService/BootstrapNode": { + "post": { + "summary": "", + "operationId": "BootstrapNode", + "responses": { + "200": { + "description": "NodeCertificateResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repositorynode.NodeCertificateResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repositorynode.BootstrapNodeRequest" + } + } + ], + "tags": [ + "repositorynodebootstrapservice.trpc" + ] + } + }, + "/trpc.ocmanager.repositorynode.RepositoryNodeInternalService/ClaimTask": { + "post": { + "summary": "", + "operationId": "ClaimTask", + "responses": { + "200": { + "description": "ClaimTaskResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repositorynode.ClaimTaskResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repositorynode.ClaimTaskRequest" + } + } + ], + "tags": [ + "repositorynodeinternalservice.trpc" + ] + } + }, + "/trpc.ocmanager.repositorynode.RepositoryNodeInternalService/ConfirmTaskCommit": { + "post": { + "summary": "", + "operationId": "ConfirmTaskCommit", + "responses": { + "200": { + "description": "A generic empty message that you can re-use to avoid defining duplicated\n empty messages in your APIs. A typical example is to use it as the request\n or the response type of an API method. For instance:\n\n service Foo {\n rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty);\n }\n\n The JSON representation for `Empty` is empty JSON object `{}`.", + "schema": { + "$ref": "#/definitions/google.protobuf.Empty" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repositorynode.ConfirmTaskCommitRequest" + } + } + ], + "tags": [ + "repositorynodeinternalservice.trpc" + ] + } + }, + "/trpc.ocmanager.repositorynode.RepositoryNodeInternalService/ExecuteDistributionTask": { + "post": { + "summary": "", + "operationId": "ExecuteDistributionTask", + "responses": { + "200": { + "description": "ExecuteDistributionTaskResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repositorynode.ExecuteDistributionTaskResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repositorynode.ExecuteDistributionTaskRequest" + } + } + ], + "tags": [ + "repositorynodeinternalservice.trpc" + ] + } + }, + "/trpc.ocmanager.repositorynode.RepositoryNodeInternalService/HeartbeatNode": { + "post": { + "summary": "", + "operationId": "HeartbeatNode", + "responses": { + "200": { + "description": "HeartbeatNodeResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repositorynode.HeartbeatNodeResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repositorynode.HeartbeatNodeRequest" + } + } + ], + "tags": [ + "repositorynodeinternalservice.trpc" + ] + } + }, + "/trpc.ocmanager.repositorynode.RepositoryNodeInternalService/HeartbeatTask": { + "post": { + "summary": "", + "operationId": "HeartbeatTask", + "responses": { + "200": { + "description": "HeartbeatTaskResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repositorynode.HeartbeatTaskResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repositorynode.HeartbeatTaskRequest" + } + } + ], + "tags": [ + "repositorynodeinternalservice.trpc" + ] + } + }, + "/trpc.ocmanager.repositorynode.RepositoryNodeInternalService/PrepareTaskCommit": { + "post": { + "summary": "", + "operationId": "PrepareTaskCommit", + "responses": { + "200": { + "description": "PrepareTaskCommitResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repositorynode.PrepareTaskCommitResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repositorynode.PrepareTaskCommitRequest" + } + } + ], + "tags": [ + "repositorynodeinternalservice.trpc" + ] + } + }, + "/trpc.ocmanager.repositorynode.RepositoryNodeInternalService/RenewNodeCertificate": { + "post": { + "summary": "", + "operationId": "RenewNodeCertificate", + "responses": { + "200": { + "description": "NodeCertificateResponse", + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repositorynode.NodeCertificateResponse" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repositorynode.RenewNodeCertificateRequest" + } + } + ], + "tags": [ + "repositorynodeinternalservice.trpc" + ] + } + }, + "/trpc.ocmanager.repositorynode.RepositoryNodeInternalService/ReportTaskFailure": { + "post": { + "summary": "", + "operationId": "ReportTaskFailure", + "responses": { + "200": { + "description": "A generic empty message that you can re-use to avoid defining duplicated\n empty messages in your APIs. A typical example is to use it as the request\n or the response type of an API method. For instance:\n\n service Foo {\n rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty);\n }\n\n The JSON representation for `Empty` is empty JSON object `{}`.", + "schema": { + "$ref": "#/definitions/google.protobuf.Empty" + } + } + }, + "parameters": [ + { + "name": "requestBody", + "in": "body", + "required": false, + "schema": { + "$ref": "#/definitions/trpc.ocmanager.repositorynode.ReportTaskFailureRequest" + } + } + ], + "tags": [ + "repositorynodeinternalservice.trpc" + ] + } + } + }, + "definitions": { + "google.protobuf.Empty": { + "type": "object", + "title": "google.protobuf.Empty", + "description": "A generic empty message that you can re-use to avoid defining duplicated\n empty messages in your APIs. A typical example is to use it as the request\n or the response type of an API method. For instance:\n\n service Foo {\n rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty);\n }\n\n The JSON representation for `Empty` is empty JSON object `{}`." + }, + "google.protobuf.Timestamp": { + "type": "object", + "properties": { + "nanos": { + "title": "nanos", + "type": "integer", + "format": "int32", + "description": "Non-negative fractions of a second at nanosecond resolution. Negative\n second values with fractions must still have non-negative nanos values\n that count forward in time. Must be from 0 to 999,999,999\n inclusive." + }, + "seconds": { + "title": "seconds", + "type": "string", + "format": "int64", + "description": "Represents seconds of UTC time since Unix epoch\n 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to\n 9999-12-31T23:59:59Z inclusive." + } + }, + "title": "google.protobuf.Timestamp", + "description": "A Timestamp represents a point in time independent of any time zone or local\n calendar, encoded as a count of seconds and fractions of seconds at\n nanosecond resolution. The count is relative to an epoch at UTC midnight on\n January 1, 1970, in the proleptic Gregorian calendar which extends the\n Gregorian calendar backwards to year one.\n\n All minutes are 60 seconds long. Leap seconds are \"smeared\" so that no leap\n second table is needed for interpretation, using a [24-hour linear\n smear](https://developers.google.com/time/smear).\n\n The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By\n restricting to that range, we ensure that we can convert to and from [RFC\n 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings.\n\n # Examples\n\n Example 1: Compute Timestamp from POSIX `time()`.\n\n Timestamp timestamp;\n timestamp.set_seconds(time(NULL));\n timestamp.set_nanos(0);\n\n Example 2: Compute Timestamp from POSIX `gettimeofday()`.\n\n struct timeval tv;\n gettimeofday(\u0026tv, NULL);\n\n Timestamp timestamp;\n timestamp.set_seconds(tv.tv_sec);\n timestamp.set_nanos(tv.tv_usec * 1000);\n\n Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`.\n\n FILETIME ft;\n GetSystemTimeAsFileTime(\u0026ft);\n UINT64 ticks = (((UINT64)ft.dwHighDateTime) \u003c\u003c 32) | ft.dwLowDateTime;\n\n // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z\n // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z.\n Timestamp timestamp;\n timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL));\n timestamp.set_nanos((INT32) ((ticks % 10000000) * 100));\n\n Example 4: Compute Timestamp from Java `System.currentTimeMillis()`.\n\n long millis = System.currentTimeMillis();\n\n Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000)\n .setNanos((int) ((millis % 1000) * 1000000)).build();\n\n\n Example 5: Compute Timestamp from Java `Instant.now()`.\n\n Instant now = Instant.now();\n\n Timestamp timestamp =\n Timestamp.newBuilder().setSeconds(now.getEpochSecond())\n .setNanos(now.getNano()).build();\n\n\n Example 6: Compute Timestamp from current time in Python.\n\n timestamp = Timestamp()\n timestamp.GetCurrentTime()\n\n # JSON Mapping\n\n In JSON format, the Timestamp type is encoded as a string in the\n [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the\n format is \"{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z\"\n where {year} is always expressed using four digits while {month}, {day},\n {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional\n seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution),\n are optional. The \"Z\" suffix indicates the timezone (\"UTC\"); the timezone\n is required. A proto3 JSON serializer should always use UTC (as indicated by\n \"Z\") when printing the Timestamp type and a proto3 JSON parser should be\n able to accept both UTC and other timezones (as indicated by an offset).\n\n For example, \"2017-01-15T01:30:15.01Z\" encodes 15.01 seconds past\n 01:30 UTC on January 15, 2017.\n\n In JavaScript, one can convert a Date object to this format using the\n standard\n [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)\n method. In Python, a standard `datetime.datetime` object can be converted\n to this format using\n [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with\n the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use\n the Joda Time's [`ISODateTimeFormat.dateTime()`](\n http://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime%2D%2D\n ) to obtain a formatter capable of generating timestamps in this format." + }, + "trpc.ocmanager.repositorynode.BootstrapNodeRequest": { + "type": "object", + "properties": { + "architecture": { + "title": "architecture", + "type": "string" + }, + "authorization_id": { + "title": "authorization_id", + "type": "string" + }, + "bootstrap_token": { + "title": "bootstrap_token", + "type": "string" + }, + "csr_pem": { + "title": "csr_pem", + "type": "string", + "format": "bytes" + }, + "enrollment_id": { + "title": "enrollment_id", + "type": "string" + }, + "install_task_id": { + "title": "install_task_id", + "type": "string" + }, + "tms_agent_id": { + "title": "tms_agent_id", + "type": "string", + "format": "uint64" + } + }, + "title": "trpc.ocmanager.repositorynode.BootstrapNodeRequest", + "description": "BootstrapNodeRequest" + }, + "trpc.ocmanager.repositorynode.ClaimTaskRequest": { + "type": "object", + "properties": { + "lease_seconds": { + "title": "lease_seconds", + "type": "integer", + "format": "int32" + }, + "storage_node_id": { + "title": "storage_node_id", + "type": "string" + }, + "worker_id": { + "title": "worker_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repositorynode.ClaimTaskRequest", + "description": "ClaimTaskRequest" + }, + "trpc.ocmanager.repositorynode.ClaimTaskResponse": { + "type": "object", + "properties": { + "task": { + "title": "task", + "$ref": "#/definitions/trpc.ocmanager.repositorynode.RepositoryTaskSpec" + } + }, + "title": "trpc.ocmanager.repositorynode.ClaimTaskResponse", + "description": "ClaimTaskResponse" + }, + "trpc.ocmanager.repositorynode.ConfirmTaskCommitRequest": { + "type": "object", + "properties": { + "commit_token": { + "title": "commit_token", + "type": "string" + }, + "fencing_token": { + "title": "fencing_token", + "type": "string", + "format": "uint64" + }, + "storage_node_id": { + "title": "storage_node_id", + "type": "string" + }, + "task_id": { + "title": "task_id", + "type": "string" + }, + "worker_id": { + "title": "worker_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repositorynode.ConfirmTaskCommitRequest", + "description": "ConfirmTaskCommitRequest" + }, + "trpc.ocmanager.repositorynode.ExecuteDistributionTaskRequest": { + "type": "object", + "properties": { + "fencing_token": { + "title": "fencing_token", + "type": "string", + "format": "uint64" + }, + "storage_node_id": { + "title": "storage_node_id", + "type": "string" + }, + "task_id": { + "title": "task_id", + "type": "string" + }, + "worker_id": { + "title": "worker_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repositorynode.ExecuteDistributionTaskRequest", + "description": "ExecuteDistributionTask keeps TMS credentials and database writes on the\n management plane. The repository-node worker only triggers the fenced task\n over mTLS; target nodes still pull the immutable revision from its public URL." + }, + "trpc.ocmanager.repositorynode.ExecuteDistributionTaskResponse": { + "type": "object", + "properties": { + "distribution_id": { + "title": "distribution_id", + "type": "string" + }, + "error_code": { + "title": "error_code", + "type": "string" + }, + "error_message": { + "title": "error_message", + "type": "string" + }, + "status": { + "title": "status", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_TASK_STATUS_UNSPECIFIED - \n * 1 - REPOSITORY_TASK_STATUS_PENDING - \n * 2 - REPOSITORY_TASK_STATUS_RUNNING - \n * 3 - REPOSITORY_TASK_STATUS_CANCEL_REQUESTED - \n * 4 - REPOSITORY_TASK_STATUS_CANCELED - \n * 5 - REPOSITORY_TASK_STATUS_SUCCESS - \n * 6 - REPOSITORY_TASK_STATUS_FAILED - \n * 7 - REPOSITORY_TASK_STATUS_PARTIAL_SUCCESS - \n", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ] + }, + "tms_task_id": { + "title": "tms_task_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repositorynode.ExecuteDistributionTaskResponse", + "description": "ExecuteDistributionTaskResponse" + }, + "trpc.ocmanager.repositorynode.HeartbeatNodeRequest": { + "type": "object", + "properties": { + "agent_version": { + "title": "agent_version", + "type": "string" + }, + "architecture": { + "title": "architecture", + "type": "string" + }, + "capacity_bytes": { + "title": "capacity_bytes", + "type": "string", + "format": "int64" + }, + "error_code": { + "title": "error_code", + "type": "string" + }, + "error_message": { + "title": "error_message", + "type": "string" + }, + "filesystem_type": { + "title": "filesystem_type", + "type": "string" + }, + "free_bytes": { + "title": "free_bytes", + "type": "string", + "format": "int64" + }, + "free_inodes": { + "title": "free_inodes", + "type": "string", + "format": "int64" + }, + "hostname": { + "title": "hostname", + "type": "string" + }, + "ip_address": { + "title": "ip_address", + "type": "string" + }, + "metadata_signing_key_snapshot": { + "title": "metadata_signing_key_snapshot", + "type": "boolean" + }, + "metadata_signing_keys": { + "title": "metadata_signing_keys", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repositorynode.MetadataSigningKeyInventory" + } + }, + "observed_at": { + "title": "observed_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "os_name": { + "title": "os_name", + "type": "string" + }, + "os_version": { + "title": "os_version", + "type": "string" + }, + "repository_usage_snapshot": { + "title": "repository_usage_snapshot", + "type": "boolean" + }, + "repository_usages": { + "title": "repository_usages", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repositorynode.RepositoryUsage" + } + }, + "storage_node_id": { + "title": "storage_node_id", + "type": "string" + }, + "tool_versions": { + "title": "tool_versions", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repositorynode.HeartbeatNodeRequest.ToolVersionsEntry" + } + } + }, + "title": "trpc.ocmanager.repositorynode.HeartbeatNodeRequest", + "description": "HeartbeatNodeRequest" + }, + "trpc.ocmanager.repositorynode.HeartbeatNodeRequest.ToolVersionsEntry": { + "type": "object", + "properties": { + "key": { + "title": "key", + "type": "string" + }, + "value": { + "title": "value", + "type": "string" + } + }, + "title": "trpc.ocmanager.repositorynode.HeartbeatNodeRequest.ToolVersionsEntry", + "description": "ToolVersionsEntry" + }, + "trpc.ocmanager.repositorynode.HeartbeatNodeResponse": { + "type": "object", + "properties": { + "next_heartbeat_seconds": { + "title": "next_heartbeat_seconds", + "type": "integer", + "format": "int32" + }, + "node_enabled": { + "title": "node_enabled", + "type": "boolean" + }, + "server_time": { + "title": "server_time", + "$ref": "#/definitions/google.protobuf.Timestamp" + } + }, + "title": "trpc.ocmanager.repositorynode.HeartbeatNodeResponse", + "description": "HeartbeatNodeResponse" + }, + "trpc.ocmanager.repositorynode.HeartbeatTaskRequest": { + "type": "object", + "properties": { + "current_step": { + "title": "current_step", + "type": "string" + }, + "fencing_token": { + "title": "fencing_token", + "type": "string", + "format": "uint64" + }, + "lease_seconds": { + "title": "lease_seconds", + "type": "integer", + "format": "int32" + }, + "progress_percent": { + "title": "progress_percent", + "type": "integer", + "format": "int32" + }, + "storage_node_id": { + "title": "storage_node_id", + "type": "string" + }, + "task_id": { + "title": "task_id", + "type": "string" + }, + "worker_id": { + "title": "worker_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repositorynode.HeartbeatTaskRequest", + "description": "HeartbeatTaskRequest" + }, + "trpc.ocmanager.repositorynode.HeartbeatTaskResponse": { + "type": "object", + "properties": { + "cancel_requested": { + "title": "cancel_requested", + "type": "boolean" + }, + "lease_expires_at": { + "title": "lease_expires_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + } + }, + "title": "trpc.ocmanager.repositorynode.HeartbeatTaskResponse", + "description": "HeartbeatTaskResponse" + }, + "trpc.ocmanager.repositorynode.MetadataSigningKeyInventory": { + "type": "object", + "properties": { + "fingerprint": { + "title": "fingerprint", + "type": "string" + }, + "key_created_at": { + "title": "key_created_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "key_ref": { + "title": "key_ref", + "type": "string" + }, + "observed_at": { + "title": "observed_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "public_key_name": { + "title": "public_key_name", + "type": "string" + }, + "public_key_url": { + "title": "public_key_url", + "type": "string" + }, + "status": { + "title": "status", + "type": "string" + } + }, + "title": "trpc.ocmanager.repositorynode.MetadataSigningKeyInventory", + "description": "MetadataSigningKeyInventory" + }, + "trpc.ocmanager.repositorynode.NodeCertificateResponse": { + "type": "object", + "properties": { + "ca_certificate_pem": { + "title": "ca_certificate_pem", + "type": "string", + "format": "bytes" + }, + "certificate_pem": { + "title": "certificate_pem", + "type": "string", + "format": "bytes" + }, + "fingerprint": { + "title": "fingerprint", + "type": "string" + }, + "not_after": { + "title": "not_after", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "not_before": { + "title": "not_before", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "serial_number": { + "title": "serial_number", + "type": "string" + }, + "storage_node_id": { + "title": "storage_node_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repositorynode.NodeCertificateResponse", + "description": "NodeCertificateResponse" + }, + "trpc.ocmanager.repositorynode.PrepareTaskCommitRequest": { + "type": "object", + "properties": { + "completion_status": { + "title": "completion_status", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_TASK_STATUS_UNSPECIFIED - \n * 1 - REPOSITORY_TASK_STATUS_PENDING - \n * 2 - REPOSITORY_TASK_STATUS_RUNNING - \n * 3 - REPOSITORY_TASK_STATUS_CANCEL_REQUESTED - \n * 4 - REPOSITORY_TASK_STATUS_CANCELED - \n * 5 - REPOSITORY_TASK_STATUS_SUCCESS - \n * 6 - REPOSITORY_TASK_STATUS_FAILED - \n * 7 - REPOSITORY_TASK_STATUS_PARTIAL_SUCCESS - \n", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ] + }, + "error_code": { + "title": "error_code", + "type": "string" + }, + "error_message": { + "title": "error_message", + "type": "string" + }, + "fencing_token": { + "title": "fencing_token", + "type": "string", + "format": "uint64" + }, + "manifest_sha256": { + "title": "manifest_sha256", + "type": "string" + }, + "package_count": { + "title": "package_count", + "type": "string", + "format": "int64" + }, + "result_object_id": { + "title": "result_object_id", + "type": "string" + }, + "result_object_sha256": { + "title": "result_object_sha256", + "type": "string" + }, + "revision_id": { + "title": "revision_id", + "type": "string" + }, + "storage_node_id": { + "title": "storage_node_id", + "type": "string" + }, + "task_id": { + "title": "task_id", + "type": "string" + }, + "total_bytes": { + "title": "total_bytes", + "type": "string", + "format": "int64" + }, + "worker_id": { + "title": "worker_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repositorynode.PrepareTaskCommitRequest", + "description": "PrepareTaskCommitRequest" + }, + "trpc.ocmanager.repositorynode.PrepareTaskCommitResponse": { + "type": "object", + "properties": { + "commit_token": { + "title": "commit_token", + "type": "string" + }, + "manifest_sha256": { + "title": "manifest_sha256", + "type": "string" + }, + "revision_id": { + "title": "revision_id", + "type": "string" + }, + "state": { + "title": "state", + "type": "string" + } + }, + "title": "trpc.ocmanager.repositorynode.PrepareTaskCommitResponse", + "description": "PrepareTaskCommitResponse" + }, + "trpc.ocmanager.repositorynode.RenewNodeCertificateRequest": { + "type": "object", + "properties": { + "csr_pem": { + "title": "csr_pem", + "type": "string", + "format": "bytes" + }, + "storage_node_id": { + "title": "storage_node_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repositorynode.RenewNodeCertificateRequest", + "description": "RenewNodeCertificateRequest" + }, + "trpc.ocmanager.repositorynode.ReportTaskFailureRequest": { + "type": "object", + "properties": { + "error_code": { + "title": "error_code", + "type": "string" + }, + "error_message": { + "title": "error_message", + "type": "string" + }, + "fencing_token": { + "title": "fencing_token", + "type": "string", + "format": "uint64" + }, + "quarantine_count": { + "title": "quarantine_count", + "type": "string", + "format": "int64" + }, + "result_object_id": { + "title": "result_object_id", + "type": "string" + }, + "result_object_sha256": { + "title": "result_object_sha256", + "type": "string" + }, + "status": { + "title": "status", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_TASK_STATUS_UNSPECIFIED - \n * 1 - REPOSITORY_TASK_STATUS_PENDING - \n * 2 - REPOSITORY_TASK_STATUS_RUNNING - \n * 3 - REPOSITORY_TASK_STATUS_CANCEL_REQUESTED - \n * 4 - REPOSITORY_TASK_STATUS_CANCELED - \n * 5 - REPOSITORY_TASK_STATUS_SUCCESS - \n * 6 - REPOSITORY_TASK_STATUS_FAILED - \n * 7 - REPOSITORY_TASK_STATUS_PARTIAL_SUCCESS - \n", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ] + }, + "storage_node_id": { + "title": "storage_node_id", + "type": "string" + }, + "task_id": { + "title": "task_id", + "type": "string" + }, + "worker_id": { + "title": "worker_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repositorynode.ReportTaskFailureRequest", + "description": "ReportTaskFailure closes a failed or canceled lease without publishing a revision.\n It is separate from the successful prepare/activate/confirm path so a failed\n data-plane execution can never mutate the active repository revision." + }, + "trpc.ocmanager.repositorynode.RepositoryGPGKeyReference": { + "type": "object", + "properties": { + "fingerprint": { + "title": "fingerprint", + "type": "string" + }, + "key_id": { + "title": "key_id", + "type": "string" + }, + "public_key_path": { + "title": "public_key_path", + "type": "string" + } + }, + "title": "trpc.ocmanager.repositorynode.RepositoryGPGKeyReference", + "description": "RepositoryGPGKeyReference" + }, + "trpc.ocmanager.repositorynode.RepositoryMetadataSigningReference": { + "type": "object", + "properties": { + "fingerprint": { + "title": "fingerprint", + "type": "string" + }, + "key_ref": { + "title": "key_ref", + "type": "string" + }, + "public_key_name": { + "title": "public_key_name", + "type": "string" + }, + "public_key_url": { + "title": "public_key_url", + "type": "string" + } + }, + "title": "trpc.ocmanager.repositorynode.RepositoryMetadataSigningReference", + "description": "RepositoryMetadataSigningReference" + }, + "trpc.ocmanager.repositorynode.RepositoryTaskSpec": { + "type": "object", + "properties": { + "active_revision_id": { + "title": "active_revision_id", + "type": "string" + }, + "architecture": { + "title": "architecture", + "type": "string" + }, + "fencing_token": { + "title": "fencing_token", + "type": "string", + "format": "uint64" + }, + "gpg_keys": { + "title": "gpg_keys", + "type": "array", + "items": { + "$ref": "#/definitions/trpc.ocmanager.repositorynode.RepositoryGPGKeyReference" + } + }, + "gpg_policy": { + "title": "gpg_policy", + "type": "string" + }, + "lease_expires_at": { + "title": "lease_expires_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "metadata_signing": { + "title": "metadata_signing", + "$ref": "#/definitions/trpc.ocmanager.repositorynode.RepositoryMetadataSigningReference" + }, + "payload_json": { + "title": "payload_json", + "type": "string" + }, + "preflight_id": { + "title": "preflight_id", + "type": "string" + }, + "publish_base_url": { + "title": "publish_base_url", + "type": "string" + }, + "publish_key": { + "title": "publish_key", + "type": "string" + }, + "repository_id": { + "title": "repository_id", + "type": "string" + }, + "revision_id": { + "title": "revision_id", + "type": "string" + }, + "storage_node_id": { + "title": "storage_node_id", + "type": "string" + }, + "task_id": { + "title": "task_id", + "type": "string" + }, + "task_type": { + "title": "task_type", + "type": "integer", + "format": "int32", + "description": " * 0 - REPOSITORY_TASK_TYPE_UNSPECIFIED - \n * 1 - REPOSITORY_TASK_TYPE_SYNC - \n * 2 - REPOSITORY_TASK_TYPE_UPLOAD - \n * 3 - REPOSITORY_TASK_TYPE_DELETE_PACKAGE - \n * 4 - REPOSITORY_TASK_TYPE_REBUILD_METADATA - \n * 5 - REPOSITORY_TASK_TYPE_DELETE_REPOSITORY - \n * 6 - REPOSITORY_TASK_TYPE_PUBLISH - \n * 7 - REPOSITORY_TASK_TYPE_VALIDATE - \n * 8 - REPOSITORY_TASK_TYPE_DISTRIBUTE - \n * 9 - REPOSITORY_TASK_TYPE_MIGRATE_REPOSITORY - \n * 10 - REPOSITORY_TASK_TYPE_NODE_INSTALL - \n * 11 - REPOSITORY_TASK_TYPE_PACKAGE_INSTALL - \n * 12 - REPOSITORY_TASK_TYPE_INVENTORY_IMPORT - \n", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12 + ] + } + }, + "title": "trpc.ocmanager.repositorynode.RepositoryTaskSpec", + "description": "RepositoryTaskSpec" + }, + "trpc.ocmanager.repositorynode.RepositoryUsage": { + "type": "object", + "properties": { + "logical_bytes": { + "title": "logical_bytes", + "type": "string", + "format": "int64" + }, + "observed_at": { + "title": "observed_at", + "$ref": "#/definitions/google.protobuf.Timestamp" + }, + "physical_bytes": { + "title": "physical_bytes", + "type": "string", + "format": "int64" + }, + "publish_status": { + "title": "publish_status", + "type": "string" + }, + "repository_id": { + "title": "repository_id", + "type": "string" + } + }, + "title": "trpc.ocmanager.repositorynode.RepositoryUsage", + "description": "RepositoryUsage" + } + } +} \ No newline at end of file diff --git a/manager/backend/protocol/repositorynode/build.sh b/manager/backend/protocol/repositorynode/build.sh new file mode 100644 index 0000000000000000000000000000000000000000..81f41a3d2aae1a5cb62eb3cd2ca4fa88cabecb3e --- /dev/null +++ b/manager/backend/protocol/repositorynode/build.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -euo pipefail + +for tool in trpc mockgen; do + if ! command -v "${tool}" >/dev/null 2>&1; then + echo "ERROR: '${tool}' not found in PATH" >&2 + exit 1 + fi +done + +candidates=( + "${HOME}/.trpc-cmdline-assets/submodules/trpc-protocol/trpc/validate" + "${HOME}/.trpc-cmdline-assets/validate" +) +validate_dir="" +for candidate in "${candidates[@]}"; do + if [[ -f "${candidate}/validate.proto" ]]; then + validate_dir="${candidate}" + break + fi +done +if [[ -z "${validate_dir}" ]]; then + echo "ERROR: validate.proto not found in trpc-cmdline assets" >&2 + exit 1 +fi + +protocol_root=$(cd "$(dirname "$0")/.." && pwd) +backend_root=$(cd "${protocol_root}/.." && pwd) +cd "${protocol_root}" +trpc create --nogomod \ + --protofile=repositorynode/repository_node.proto \ + --rpconly=true \ + --protodir=. --protodir="${validate_dir}" \ + --domain=trpc.group \ + --output=repositorynode --goversion=1.18 \ + --validate=true --mock=false \ + --swagger --swagger-json-param --usebasename=false +rm -f repositorynode/go.mod repositorynode/go.sum +cd "${backend_root}" +mockgen -source=protocol/repositorynode/repository_node.trpc.go \ + -destination=protocol/repositorynode/repository_node_mock.go -package=repositorynode diff --git a/manager/backend/protocol/repositorynode/repository_node.pb.go b/manager/backend/protocol/repositorynode/repository_node.pb.go new file mode 100644 index 0000000000000000000000000000000000000000..ed69bfb2e080e9624c050f086c8436838782c6ae --- /dev/null +++ b/manager/backend/protocol/repositorynode/repository_node.pb.go @@ -0,0 +1,2698 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.26.0 +// protoc v4.24.2 +// source: repositorynode/repository_node.proto + +package repositorynode + +import ( + reflect "reflect" + sync "sync" + + repository "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + _ "trpc.group/trpc/trpc-protocol/pb/go/trpc/validate" +) + +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 HeartbeatNodeRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + StorageNodeId string `protobuf:"bytes,1,opt,name=storage_node_id,json=storageNodeId,proto3" json:"storage_node_id,omitempty"` + FilesystemType string `protobuf:"bytes,2,opt,name=filesystem_type,json=filesystemType,proto3" json:"filesystem_type,omitempty"` + CapacityBytes int64 `protobuf:"varint,3,opt,name=capacity_bytes,json=capacityBytes,proto3" json:"capacity_bytes,omitempty"` + FreeBytes int64 `protobuf:"varint,4,opt,name=free_bytes,json=freeBytes,proto3" json:"free_bytes,omitempty"` + FreeInodes int64 `protobuf:"varint,5,opt,name=free_inodes,json=freeInodes,proto3" json:"free_inodes,omitempty"` + ToolVersions map[string]string `protobuf:"bytes,6,rep,name=tool_versions,json=toolVersions,proto3" json:"tool_versions,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + ObservedAt *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=observed_at,json=observedAt,proto3" json:"observed_at,omitempty"` + ErrorCode string `protobuf:"bytes,8,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"` + ErrorMessage string `protobuf:"bytes,9,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` + Hostname string `protobuf:"bytes,10,opt,name=hostname,proto3" json:"hostname,omitempty"` + IpAddress string `protobuf:"bytes,11,opt,name=ip_address,json=ipAddress,proto3" json:"ip_address,omitempty"` + Architecture string `protobuf:"bytes,12,opt,name=architecture,proto3" json:"architecture,omitempty"` + OsName string `protobuf:"bytes,13,opt,name=os_name,json=osName,proto3" json:"os_name,omitempty"` + OsVersion string `protobuf:"bytes,14,opt,name=os_version,json=osVersion,proto3" json:"os_version,omitempty"` + AgentVersion string `protobuf:"bytes,15,opt,name=agent_version,json=agentVersion,proto3" json:"agent_version,omitempty"` + RepositoryUsages []*RepositoryUsage `protobuf:"bytes,16,rep,name=repository_usages,json=repositoryUsages,proto3" json:"repository_usages,omitempty"` + RepositoryUsageSnapshot bool `protobuf:"varint,17,opt,name=repository_usage_snapshot,json=repositoryUsageSnapshot,proto3" json:"repository_usage_snapshot,omitempty"` + MetadataSigningKeys []*MetadataSigningKeyInventory `protobuf:"bytes,18,rep,name=metadata_signing_keys,json=metadataSigningKeys,proto3" json:"metadata_signing_keys,omitempty"` + MetadataSigningKeySnapshot bool `protobuf:"varint,19,opt,name=metadata_signing_key_snapshot,json=metadataSigningKeySnapshot,proto3" json:"metadata_signing_key_snapshot,omitempty"` +} + +func (x *HeartbeatNodeRequest) Reset() { + *x = HeartbeatNodeRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repositorynode_repository_node_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HeartbeatNodeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HeartbeatNodeRequest) ProtoMessage() {} + +func (x *HeartbeatNodeRequest) ProtoReflect() protoreflect.Message { + mi := &file_repositorynode_repository_node_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HeartbeatNodeRequest.ProtoReflect.Descriptor instead. +func (*HeartbeatNodeRequest) Descriptor() ([]byte, []int) { + return file_repositorynode_repository_node_proto_rawDescGZIP(), []int{0} +} + +func (x *HeartbeatNodeRequest) GetStorageNodeId() string { + if x != nil { + return x.StorageNodeId + } + return "" +} + +func (x *HeartbeatNodeRequest) GetFilesystemType() string { + if x != nil { + return x.FilesystemType + } + return "" +} + +func (x *HeartbeatNodeRequest) GetCapacityBytes() int64 { + if x != nil { + return x.CapacityBytes + } + return 0 +} + +func (x *HeartbeatNodeRequest) GetFreeBytes() int64 { + if x != nil { + return x.FreeBytes + } + return 0 +} + +func (x *HeartbeatNodeRequest) GetFreeInodes() int64 { + if x != nil { + return x.FreeInodes + } + return 0 +} + +func (x *HeartbeatNodeRequest) GetToolVersions() map[string]string { + if x != nil { + return x.ToolVersions + } + return nil +} + +func (x *HeartbeatNodeRequest) GetObservedAt() *timestamppb.Timestamp { + if x != nil { + return x.ObservedAt + } + return nil +} + +func (x *HeartbeatNodeRequest) GetErrorCode() string { + if x != nil { + return x.ErrorCode + } + return "" +} + +func (x *HeartbeatNodeRequest) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +func (x *HeartbeatNodeRequest) GetHostname() string { + if x != nil { + return x.Hostname + } + return "" +} + +func (x *HeartbeatNodeRequest) GetIpAddress() string { + if x != nil { + return x.IpAddress + } + return "" +} + +func (x *HeartbeatNodeRequest) GetArchitecture() string { + if x != nil { + return x.Architecture + } + return "" +} + +func (x *HeartbeatNodeRequest) GetOsName() string { + if x != nil { + return x.OsName + } + return "" +} + +func (x *HeartbeatNodeRequest) GetOsVersion() string { + if x != nil { + return x.OsVersion + } + return "" +} + +func (x *HeartbeatNodeRequest) GetAgentVersion() string { + if x != nil { + return x.AgentVersion + } + return "" +} + +func (x *HeartbeatNodeRequest) GetRepositoryUsages() []*RepositoryUsage { + if x != nil { + return x.RepositoryUsages + } + return nil +} + +func (x *HeartbeatNodeRequest) GetRepositoryUsageSnapshot() bool { + if x != nil { + return x.RepositoryUsageSnapshot + } + return false +} + +func (x *HeartbeatNodeRequest) GetMetadataSigningKeys() []*MetadataSigningKeyInventory { + if x != nil { + return x.MetadataSigningKeys + } + return nil +} + +func (x *HeartbeatNodeRequest) GetMetadataSigningKeySnapshot() bool { + if x != nil { + return x.MetadataSigningKeySnapshot + } + return false +} + +type MetadataSigningKeyInventory struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + KeyRef string `protobuf:"bytes,1,opt,name=key_ref,json=keyRef,proto3" json:"key_ref,omitempty"` + Fingerprint string `protobuf:"bytes,2,opt,name=fingerprint,proto3" json:"fingerprint,omitempty"` + PublicKeyName string `protobuf:"bytes,3,opt,name=public_key_name,json=publicKeyName,proto3" json:"public_key_name,omitempty"` + PublicKeyUrl string `protobuf:"bytes,4,opt,name=public_key_url,json=publicKeyUrl,proto3" json:"public_key_url,omitempty"` + Status string `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"` + KeyCreatedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=key_created_at,json=keyCreatedAt,proto3" json:"key_created_at,omitempty"` + ObservedAt *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=observed_at,json=observedAt,proto3" json:"observed_at,omitempty"` +} + +func (x *MetadataSigningKeyInventory) Reset() { + *x = MetadataSigningKeyInventory{} + if protoimpl.UnsafeEnabled { + mi := &file_repositorynode_repository_node_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MetadataSigningKeyInventory) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MetadataSigningKeyInventory) ProtoMessage() {} + +func (x *MetadataSigningKeyInventory) ProtoReflect() protoreflect.Message { + mi := &file_repositorynode_repository_node_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MetadataSigningKeyInventory.ProtoReflect.Descriptor instead. +func (*MetadataSigningKeyInventory) Descriptor() ([]byte, []int) { + return file_repositorynode_repository_node_proto_rawDescGZIP(), []int{1} +} + +func (x *MetadataSigningKeyInventory) GetKeyRef() string { + if x != nil { + return x.KeyRef + } + return "" +} + +func (x *MetadataSigningKeyInventory) GetFingerprint() string { + if x != nil { + return x.Fingerprint + } + return "" +} + +func (x *MetadataSigningKeyInventory) GetPublicKeyName() string { + if x != nil { + return x.PublicKeyName + } + return "" +} + +func (x *MetadataSigningKeyInventory) GetPublicKeyUrl() string { + if x != nil { + return x.PublicKeyUrl + } + return "" +} + +func (x *MetadataSigningKeyInventory) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *MetadataSigningKeyInventory) GetKeyCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.KeyCreatedAt + } + return nil +} + +func (x *MetadataSigningKeyInventory) GetObservedAt() *timestamppb.Timestamp { + if x != nil { + return x.ObservedAt + } + return nil +} + +type RepositoryUsage struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RepositoryId string `protobuf:"bytes,1,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + LogicalBytes int64 `protobuf:"varint,2,opt,name=logical_bytes,json=logicalBytes,proto3" json:"logical_bytes,omitempty"` + PhysicalBytes int64 `protobuf:"varint,3,opt,name=physical_bytes,json=physicalBytes,proto3" json:"physical_bytes,omitempty"` + PublishStatus string `protobuf:"bytes,4,opt,name=publish_status,json=publishStatus,proto3" json:"publish_status,omitempty"` + ObservedAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=observed_at,json=observedAt,proto3" json:"observed_at,omitempty"` +} + +func (x *RepositoryUsage) Reset() { + *x = RepositoryUsage{} + if protoimpl.UnsafeEnabled { + mi := &file_repositorynode_repository_node_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryUsage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryUsage) ProtoMessage() {} + +func (x *RepositoryUsage) ProtoReflect() protoreflect.Message { + mi := &file_repositorynode_repository_node_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryUsage.ProtoReflect.Descriptor instead. +func (*RepositoryUsage) Descriptor() ([]byte, []int) { + return file_repositorynode_repository_node_proto_rawDescGZIP(), []int{2} +} + +func (x *RepositoryUsage) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *RepositoryUsage) GetLogicalBytes() int64 { + if x != nil { + return x.LogicalBytes + } + return 0 +} + +func (x *RepositoryUsage) GetPhysicalBytes() int64 { + if x != nil { + return x.PhysicalBytes + } + return 0 +} + +func (x *RepositoryUsage) GetPublishStatus() string { + if x != nil { + return x.PublishStatus + } + return "" +} + +func (x *RepositoryUsage) GetObservedAt() *timestamppb.Timestamp { + if x != nil { + return x.ObservedAt + } + return nil +} + +type HeartbeatNodeResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ServerTime *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=server_time,json=serverTime,proto3" json:"server_time,omitempty"` + NextHeartbeatSeconds int32 `protobuf:"varint,2,opt,name=next_heartbeat_seconds,json=nextHeartbeatSeconds,proto3" json:"next_heartbeat_seconds,omitempty"` + NodeEnabled bool `protobuf:"varint,3,opt,name=node_enabled,json=nodeEnabled,proto3" json:"node_enabled,omitempty"` +} + +func (x *HeartbeatNodeResponse) Reset() { + *x = HeartbeatNodeResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repositorynode_repository_node_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HeartbeatNodeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HeartbeatNodeResponse) ProtoMessage() {} + +func (x *HeartbeatNodeResponse) ProtoReflect() protoreflect.Message { + mi := &file_repositorynode_repository_node_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HeartbeatNodeResponse.ProtoReflect.Descriptor instead. +func (*HeartbeatNodeResponse) Descriptor() ([]byte, []int) { + return file_repositorynode_repository_node_proto_rawDescGZIP(), []int{3} +} + +func (x *HeartbeatNodeResponse) GetServerTime() *timestamppb.Timestamp { + if x != nil { + return x.ServerTime + } + return nil +} + +func (x *HeartbeatNodeResponse) GetNextHeartbeatSeconds() int32 { + if x != nil { + return x.NextHeartbeatSeconds + } + return 0 +} + +func (x *HeartbeatNodeResponse) GetNodeEnabled() bool { + if x != nil { + return x.NodeEnabled + } + return false +} + +type BootstrapNodeRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + EnrollmentId string `protobuf:"bytes,1,opt,name=enrollment_id,json=enrollmentId,proto3" json:"enrollment_id,omitempty"` + InstallTaskId string `protobuf:"bytes,2,opt,name=install_task_id,json=installTaskId,proto3" json:"install_task_id,omitempty"` + TmsAgentId uint64 `protobuf:"varint,3,opt,name=tms_agent_id,json=tmsAgentId,proto3" json:"tms_agent_id,omitempty"` + BootstrapToken string `protobuf:"bytes,4,opt,name=bootstrap_token,json=bootstrapToken,proto3" json:"bootstrap_token,omitempty"` + CsrPem []byte `protobuf:"bytes,5,opt,name=csr_pem,json=csrPem,proto3" json:"csr_pem,omitempty"` + AuthorizationId string `protobuf:"bytes,6,opt,name=authorization_id,json=authorizationId,proto3" json:"authorization_id,omitempty"` + Architecture string `protobuf:"bytes,7,opt,name=architecture,proto3" json:"architecture,omitempty"` +} + +func (x *BootstrapNodeRequest) Reset() { + *x = BootstrapNodeRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repositorynode_repository_node_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *BootstrapNodeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BootstrapNodeRequest) ProtoMessage() {} + +func (x *BootstrapNodeRequest) ProtoReflect() protoreflect.Message { + mi := &file_repositorynode_repository_node_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BootstrapNodeRequest.ProtoReflect.Descriptor instead. +func (*BootstrapNodeRequest) Descriptor() ([]byte, []int) { + return file_repositorynode_repository_node_proto_rawDescGZIP(), []int{4} +} + +func (x *BootstrapNodeRequest) GetEnrollmentId() string { + if x != nil { + return x.EnrollmentId + } + return "" +} + +func (x *BootstrapNodeRequest) GetInstallTaskId() string { + if x != nil { + return x.InstallTaskId + } + return "" +} + +func (x *BootstrapNodeRequest) GetTmsAgentId() uint64 { + if x != nil { + return x.TmsAgentId + } + return 0 +} + +func (x *BootstrapNodeRequest) GetBootstrapToken() string { + if x != nil { + return x.BootstrapToken + } + return "" +} + +func (x *BootstrapNodeRequest) GetCsrPem() []byte { + if x != nil { + return x.CsrPem + } + return nil +} + +func (x *BootstrapNodeRequest) GetAuthorizationId() string { + if x != nil { + return x.AuthorizationId + } + return "" +} + +func (x *BootstrapNodeRequest) GetArchitecture() string { + if x != nil { + return x.Architecture + } + return "" +} + +type RenewNodeCertificateRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + StorageNodeId string `protobuf:"bytes,1,opt,name=storage_node_id,json=storageNodeId,proto3" json:"storage_node_id,omitempty"` + CsrPem []byte `protobuf:"bytes,2,opt,name=csr_pem,json=csrPem,proto3" json:"csr_pem,omitempty"` +} + +func (x *RenewNodeCertificateRequest) Reset() { + *x = RenewNodeCertificateRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repositorynode_repository_node_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RenewNodeCertificateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RenewNodeCertificateRequest) ProtoMessage() {} + +func (x *RenewNodeCertificateRequest) ProtoReflect() protoreflect.Message { + mi := &file_repositorynode_repository_node_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RenewNodeCertificateRequest.ProtoReflect.Descriptor instead. +func (*RenewNodeCertificateRequest) Descriptor() ([]byte, []int) { + return file_repositorynode_repository_node_proto_rawDescGZIP(), []int{5} +} + +func (x *RenewNodeCertificateRequest) GetStorageNodeId() string { + if x != nil { + return x.StorageNodeId + } + return "" +} + +func (x *RenewNodeCertificateRequest) GetCsrPem() []byte { + if x != nil { + return x.CsrPem + } + return nil +} + +type NodeCertificateResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + StorageNodeId string `protobuf:"bytes,1,opt,name=storage_node_id,json=storageNodeId,proto3" json:"storage_node_id,omitempty"` + CertificatePem []byte `protobuf:"bytes,2,opt,name=certificate_pem,json=certificatePem,proto3" json:"certificate_pem,omitempty"` + CaCertificatePem []byte `protobuf:"bytes,3,opt,name=ca_certificate_pem,json=caCertificatePem,proto3" json:"ca_certificate_pem,omitempty"` + SerialNumber string `protobuf:"bytes,4,opt,name=serial_number,json=serialNumber,proto3" json:"serial_number,omitempty"` + Fingerprint string `protobuf:"bytes,5,opt,name=fingerprint,proto3" json:"fingerprint,omitempty"` + NotBefore *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=not_before,json=notBefore,proto3" json:"not_before,omitempty"` + NotAfter *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=not_after,json=notAfter,proto3" json:"not_after,omitempty"` +} + +func (x *NodeCertificateResponse) Reset() { + *x = NodeCertificateResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repositorynode_repository_node_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *NodeCertificateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NodeCertificateResponse) ProtoMessage() {} + +func (x *NodeCertificateResponse) ProtoReflect() protoreflect.Message { + mi := &file_repositorynode_repository_node_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NodeCertificateResponse.ProtoReflect.Descriptor instead. +func (*NodeCertificateResponse) Descriptor() ([]byte, []int) { + return file_repositorynode_repository_node_proto_rawDescGZIP(), []int{6} +} + +func (x *NodeCertificateResponse) GetStorageNodeId() string { + if x != nil { + return x.StorageNodeId + } + return "" +} + +func (x *NodeCertificateResponse) GetCertificatePem() []byte { + if x != nil { + return x.CertificatePem + } + return nil +} + +func (x *NodeCertificateResponse) GetCaCertificatePem() []byte { + if x != nil { + return x.CaCertificatePem + } + return nil +} + +func (x *NodeCertificateResponse) GetSerialNumber() string { + if x != nil { + return x.SerialNumber + } + return "" +} + +func (x *NodeCertificateResponse) GetFingerprint() string { + if x != nil { + return x.Fingerprint + } + return "" +} + +func (x *NodeCertificateResponse) GetNotBefore() *timestamppb.Timestamp { + if x != nil { + return x.NotBefore + } + return nil +} + +func (x *NodeCertificateResponse) GetNotAfter() *timestamppb.Timestamp { + if x != nil { + return x.NotAfter + } + return nil +} + +type ClaimTaskRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + StorageNodeId string `protobuf:"bytes,1,opt,name=storage_node_id,json=storageNodeId,proto3" json:"storage_node_id,omitempty"` + WorkerId string `protobuf:"bytes,2,opt,name=worker_id,json=workerId,proto3" json:"worker_id,omitempty"` + LeaseSeconds int32 `protobuf:"varint,3,opt,name=lease_seconds,json=leaseSeconds,proto3" json:"lease_seconds,omitempty"` +} + +func (x *ClaimTaskRequest) Reset() { + *x = ClaimTaskRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repositorynode_repository_node_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ClaimTaskRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClaimTaskRequest) ProtoMessage() {} + +func (x *ClaimTaskRequest) ProtoReflect() protoreflect.Message { + mi := &file_repositorynode_repository_node_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClaimTaskRequest.ProtoReflect.Descriptor instead. +func (*ClaimTaskRequest) Descriptor() ([]byte, []int) { + return file_repositorynode_repository_node_proto_rawDescGZIP(), []int{7} +} + +func (x *ClaimTaskRequest) GetStorageNodeId() string { + if x != nil { + return x.StorageNodeId + } + return "" +} + +func (x *ClaimTaskRequest) GetWorkerId() string { + if x != nil { + return x.WorkerId + } + return "" +} + +func (x *ClaimTaskRequest) GetLeaseSeconds() int32 { + if x != nil { + return x.LeaseSeconds + } + return 0 +} + +type RepositoryTaskSpec struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + RepositoryId string `protobuf:"bytes,2,opt,name=repository_id,json=repositoryId,proto3" json:"repository_id,omitempty"` + StorageNodeId string `protobuf:"bytes,3,opt,name=storage_node_id,json=storageNodeId,proto3" json:"storage_node_id,omitempty"` + PreflightId string `protobuf:"bytes,4,opt,name=preflight_id,json=preflightId,proto3" json:"preflight_id,omitempty"` + RevisionId string `protobuf:"bytes,5,opt,name=revision_id,json=revisionId,proto3" json:"revision_id,omitempty"` + TaskType repository.RepositoryTaskType `protobuf:"varint,6,opt,name=task_type,json=taskType,proto3,enum=trpc.ocmanager.repository.RepositoryTaskType" json:"task_type,omitempty"` + FencingToken uint64 `protobuf:"varint,7,opt,name=fencing_token,json=fencingToken,proto3" json:"fencing_token,omitempty"` + LeaseExpiresAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=lease_expires_at,json=leaseExpiresAt,proto3" json:"lease_expires_at,omitempty"` + PayloadJson string `protobuf:"bytes,9,opt,name=payload_json,json=payloadJson,proto3" json:"payload_json,omitempty"` + PublishKey string `protobuf:"bytes,10,opt,name=publish_key,json=publishKey,proto3" json:"publish_key,omitempty"` + Architecture string `protobuf:"bytes,11,opt,name=architecture,proto3" json:"architecture,omitempty"` + GpgPolicy string `protobuf:"bytes,12,opt,name=gpg_policy,json=gpgPolicy,proto3" json:"gpg_policy,omitempty"` + ActiveRevisionId string `protobuf:"bytes,13,opt,name=active_revision_id,json=activeRevisionId,proto3" json:"active_revision_id,omitempty"` + GpgKeys []*RepositoryGPGKeyReference `protobuf:"bytes,14,rep,name=gpg_keys,json=gpgKeys,proto3" json:"gpg_keys,omitempty"` + MetadataSigning *RepositoryMetadataSigningReference `protobuf:"bytes,15,opt,name=metadata_signing,json=metadataSigning,proto3" json:"metadata_signing,omitempty"` + PublishBaseUrl string `protobuf:"bytes,16,opt,name=publish_base_url,json=publishBaseUrl,proto3" json:"publish_base_url,omitempty"` +} + +func (x *RepositoryTaskSpec) Reset() { + *x = RepositoryTaskSpec{} + if protoimpl.UnsafeEnabled { + mi := &file_repositorynode_repository_node_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryTaskSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryTaskSpec) ProtoMessage() {} + +func (x *RepositoryTaskSpec) ProtoReflect() protoreflect.Message { + mi := &file_repositorynode_repository_node_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryTaskSpec.ProtoReflect.Descriptor instead. +func (*RepositoryTaskSpec) Descriptor() ([]byte, []int) { + return file_repositorynode_repository_node_proto_rawDescGZIP(), []int{8} +} + +func (x *RepositoryTaskSpec) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *RepositoryTaskSpec) GetRepositoryId() string { + if x != nil { + return x.RepositoryId + } + return "" +} + +func (x *RepositoryTaskSpec) GetStorageNodeId() string { + if x != nil { + return x.StorageNodeId + } + return "" +} + +func (x *RepositoryTaskSpec) GetPreflightId() string { + if x != nil { + return x.PreflightId + } + return "" +} + +func (x *RepositoryTaskSpec) GetRevisionId() string { + if x != nil { + return x.RevisionId + } + return "" +} + +func (x *RepositoryTaskSpec) GetTaskType() repository.RepositoryTaskType { + if x != nil { + return x.TaskType + } + return repository.RepositoryTaskType_REPOSITORY_TASK_TYPE_UNSPECIFIED +} + +func (x *RepositoryTaskSpec) GetFencingToken() uint64 { + if x != nil { + return x.FencingToken + } + return 0 +} + +func (x *RepositoryTaskSpec) GetLeaseExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.LeaseExpiresAt + } + return nil +} + +func (x *RepositoryTaskSpec) GetPayloadJson() string { + if x != nil { + return x.PayloadJson + } + return "" +} + +func (x *RepositoryTaskSpec) GetPublishKey() string { + if x != nil { + return x.PublishKey + } + return "" +} + +func (x *RepositoryTaskSpec) GetArchitecture() string { + if x != nil { + return x.Architecture + } + return "" +} + +func (x *RepositoryTaskSpec) GetGpgPolicy() string { + if x != nil { + return x.GpgPolicy + } + return "" +} + +func (x *RepositoryTaskSpec) GetActiveRevisionId() string { + if x != nil { + return x.ActiveRevisionId + } + return "" +} + +func (x *RepositoryTaskSpec) GetGpgKeys() []*RepositoryGPGKeyReference { + if x != nil { + return x.GpgKeys + } + return nil +} + +func (x *RepositoryTaskSpec) GetMetadataSigning() *RepositoryMetadataSigningReference { + if x != nil { + return x.MetadataSigning + } + return nil +} + +func (x *RepositoryTaskSpec) GetPublishBaseUrl() string { + if x != nil { + return x.PublishBaseUrl + } + return "" +} + +type RepositoryMetadataSigningReference struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + KeyRef string `protobuf:"bytes,1,opt,name=key_ref,json=keyRef,proto3" json:"key_ref,omitempty"` + Fingerprint string `protobuf:"bytes,2,opt,name=fingerprint,proto3" json:"fingerprint,omitempty"` + PublicKeyName string `protobuf:"bytes,3,opt,name=public_key_name,json=publicKeyName,proto3" json:"public_key_name,omitempty"` + PublicKeyUrl string `protobuf:"bytes,4,opt,name=public_key_url,json=publicKeyUrl,proto3" json:"public_key_url,omitempty"` +} + +func (x *RepositoryMetadataSigningReference) Reset() { + *x = RepositoryMetadataSigningReference{} + if protoimpl.UnsafeEnabled { + mi := &file_repositorynode_repository_node_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryMetadataSigningReference) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryMetadataSigningReference) ProtoMessage() {} + +func (x *RepositoryMetadataSigningReference) ProtoReflect() protoreflect.Message { + mi := &file_repositorynode_repository_node_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryMetadataSigningReference.ProtoReflect.Descriptor instead. +func (*RepositoryMetadataSigningReference) Descriptor() ([]byte, []int) { + return file_repositorynode_repository_node_proto_rawDescGZIP(), []int{9} +} + +func (x *RepositoryMetadataSigningReference) GetKeyRef() string { + if x != nil { + return x.KeyRef + } + return "" +} + +func (x *RepositoryMetadataSigningReference) GetFingerprint() string { + if x != nil { + return x.Fingerprint + } + return "" +} + +func (x *RepositoryMetadataSigningReference) GetPublicKeyName() string { + if x != nil { + return x.PublicKeyName + } + return "" +} + +func (x *RepositoryMetadataSigningReference) GetPublicKeyUrl() string { + if x != nil { + return x.PublicKeyUrl + } + return "" +} + +type RepositoryGPGKeyReference struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + KeyId string `protobuf:"bytes,1,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty"` + Fingerprint string `protobuf:"bytes,2,opt,name=fingerprint,proto3" json:"fingerprint,omitempty"` + PublicKeyPath string `protobuf:"bytes,3,opt,name=public_key_path,json=publicKeyPath,proto3" json:"public_key_path,omitempty"` +} + +func (x *RepositoryGPGKeyReference) Reset() { + *x = RepositoryGPGKeyReference{} + if protoimpl.UnsafeEnabled { + mi := &file_repositorynode_repository_node_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RepositoryGPGKeyReference) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RepositoryGPGKeyReference) ProtoMessage() {} + +func (x *RepositoryGPGKeyReference) ProtoReflect() protoreflect.Message { + mi := &file_repositorynode_repository_node_proto_msgTypes[10] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RepositoryGPGKeyReference.ProtoReflect.Descriptor instead. +func (*RepositoryGPGKeyReference) Descriptor() ([]byte, []int) { + return file_repositorynode_repository_node_proto_rawDescGZIP(), []int{10} +} + +func (x *RepositoryGPGKeyReference) GetKeyId() string { + if x != nil { + return x.KeyId + } + return "" +} + +func (x *RepositoryGPGKeyReference) GetFingerprint() string { + if x != nil { + return x.Fingerprint + } + return "" +} + +func (x *RepositoryGPGKeyReference) GetPublicKeyPath() string { + if x != nil { + return x.PublicKeyPath + } + return "" +} + +type ClaimTaskResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Task *RepositoryTaskSpec `protobuf:"bytes,1,opt,name=task,proto3" json:"task,omitempty"` +} + +func (x *ClaimTaskResponse) Reset() { + *x = ClaimTaskResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repositorynode_repository_node_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ClaimTaskResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClaimTaskResponse) ProtoMessage() {} + +func (x *ClaimTaskResponse) ProtoReflect() protoreflect.Message { + mi := &file_repositorynode_repository_node_proto_msgTypes[11] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClaimTaskResponse.ProtoReflect.Descriptor instead. +func (*ClaimTaskResponse) Descriptor() ([]byte, []int) { + return file_repositorynode_repository_node_proto_rawDescGZIP(), []int{11} +} + +func (x *ClaimTaskResponse) GetTask() *RepositoryTaskSpec { + if x != nil { + return x.Task + } + return nil +} + +type HeartbeatTaskRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + WorkerId string `protobuf:"bytes,2,opt,name=worker_id,json=workerId,proto3" json:"worker_id,omitempty"` + FencingToken uint64 `protobuf:"varint,3,opt,name=fencing_token,json=fencingToken,proto3" json:"fencing_token,omitempty"` + LeaseSeconds int32 `protobuf:"varint,4,opt,name=lease_seconds,json=leaseSeconds,proto3" json:"lease_seconds,omitempty"` + ProgressPercent int32 `protobuf:"varint,5,opt,name=progress_percent,json=progressPercent,proto3" json:"progress_percent,omitempty"` + CurrentStep string `protobuf:"bytes,6,opt,name=current_step,json=currentStep,proto3" json:"current_step,omitempty"` + StorageNodeId string `protobuf:"bytes,7,opt,name=storage_node_id,json=storageNodeId,proto3" json:"storage_node_id,omitempty"` +} + +func (x *HeartbeatTaskRequest) Reset() { + *x = HeartbeatTaskRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repositorynode_repository_node_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HeartbeatTaskRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HeartbeatTaskRequest) ProtoMessage() {} + +func (x *HeartbeatTaskRequest) ProtoReflect() protoreflect.Message { + mi := &file_repositorynode_repository_node_proto_msgTypes[12] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HeartbeatTaskRequest.ProtoReflect.Descriptor instead. +func (*HeartbeatTaskRequest) Descriptor() ([]byte, []int) { + return file_repositorynode_repository_node_proto_rawDescGZIP(), []int{12} +} + +func (x *HeartbeatTaskRequest) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *HeartbeatTaskRequest) GetWorkerId() string { + if x != nil { + return x.WorkerId + } + return "" +} + +func (x *HeartbeatTaskRequest) GetFencingToken() uint64 { + if x != nil { + return x.FencingToken + } + return 0 +} + +func (x *HeartbeatTaskRequest) GetLeaseSeconds() int32 { + if x != nil { + return x.LeaseSeconds + } + return 0 +} + +func (x *HeartbeatTaskRequest) GetProgressPercent() int32 { + if x != nil { + return x.ProgressPercent + } + return 0 +} + +func (x *HeartbeatTaskRequest) GetCurrentStep() string { + if x != nil { + return x.CurrentStep + } + return "" +} + +func (x *HeartbeatTaskRequest) GetStorageNodeId() string { + if x != nil { + return x.StorageNodeId + } + return "" +} + +type HeartbeatTaskResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + LeaseExpiresAt *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=lease_expires_at,json=leaseExpiresAt,proto3" json:"lease_expires_at,omitempty"` + CancelRequested bool `protobuf:"varint,2,opt,name=cancel_requested,json=cancelRequested,proto3" json:"cancel_requested,omitempty"` +} + +func (x *HeartbeatTaskResponse) Reset() { + *x = HeartbeatTaskResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repositorynode_repository_node_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HeartbeatTaskResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HeartbeatTaskResponse) ProtoMessage() {} + +func (x *HeartbeatTaskResponse) ProtoReflect() protoreflect.Message { + mi := &file_repositorynode_repository_node_proto_msgTypes[13] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HeartbeatTaskResponse.ProtoReflect.Descriptor instead. +func (*HeartbeatTaskResponse) Descriptor() ([]byte, []int) { + return file_repositorynode_repository_node_proto_rawDescGZIP(), []int{13} +} + +func (x *HeartbeatTaskResponse) GetLeaseExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.LeaseExpiresAt + } + return nil +} + +func (x *HeartbeatTaskResponse) GetCancelRequested() bool { + if x != nil { + return x.CancelRequested + } + return false +} + +// ReportTaskFailure closes a failed or canceled lease without publishing a revision. +// It is separate from the successful prepare/activate/confirm path so a failed +// data-plane execution can never mutate the active repository revision. +type ReportTaskFailureRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + WorkerId string `protobuf:"bytes,2,opt,name=worker_id,json=workerId,proto3" json:"worker_id,omitempty"` + FencingToken uint64 `protobuf:"varint,3,opt,name=fencing_token,json=fencingToken,proto3" json:"fencing_token,omitempty"` + StorageNodeId string `protobuf:"bytes,4,opt,name=storage_node_id,json=storageNodeId,proto3" json:"storage_node_id,omitempty"` + Status repository.RepositoryTaskStatus `protobuf:"varint,5,opt,name=status,proto3,enum=trpc.ocmanager.repository.RepositoryTaskStatus" json:"status,omitempty"` + ErrorCode string `protobuf:"bytes,6,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"` + ErrorMessage string `protobuf:"bytes,7,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` + ResultObjectId string `protobuf:"bytes,8,opt,name=result_object_id,json=resultObjectId,proto3" json:"result_object_id,omitempty"` + ResultObjectSha256 string `protobuf:"bytes,9,opt,name=result_object_sha256,json=resultObjectSha256,proto3" json:"result_object_sha256,omitempty"` + QuarantineCount int64 `protobuf:"varint,10,opt,name=quarantine_count,json=quarantineCount,proto3" json:"quarantine_count,omitempty"` +} + +func (x *ReportTaskFailureRequest) Reset() { + *x = ReportTaskFailureRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repositorynode_repository_node_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ReportTaskFailureRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportTaskFailureRequest) ProtoMessage() {} + +func (x *ReportTaskFailureRequest) ProtoReflect() protoreflect.Message { + mi := &file_repositorynode_repository_node_proto_msgTypes[14] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReportTaskFailureRequest.ProtoReflect.Descriptor instead. +func (*ReportTaskFailureRequest) Descriptor() ([]byte, []int) { + return file_repositorynode_repository_node_proto_rawDescGZIP(), []int{14} +} + +func (x *ReportTaskFailureRequest) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *ReportTaskFailureRequest) GetWorkerId() string { + if x != nil { + return x.WorkerId + } + return "" +} + +func (x *ReportTaskFailureRequest) GetFencingToken() uint64 { + if x != nil { + return x.FencingToken + } + return 0 +} + +func (x *ReportTaskFailureRequest) GetStorageNodeId() string { + if x != nil { + return x.StorageNodeId + } + return "" +} + +func (x *ReportTaskFailureRequest) GetStatus() repository.RepositoryTaskStatus { + if x != nil { + return x.Status + } + return repository.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_UNSPECIFIED +} + +func (x *ReportTaskFailureRequest) GetErrorCode() string { + if x != nil { + return x.ErrorCode + } + return "" +} + +func (x *ReportTaskFailureRequest) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +func (x *ReportTaskFailureRequest) GetResultObjectId() string { + if x != nil { + return x.ResultObjectId + } + return "" +} + +func (x *ReportTaskFailureRequest) GetResultObjectSha256() string { + if x != nil { + return x.ResultObjectSha256 + } + return "" +} + +func (x *ReportTaskFailureRequest) GetQuarantineCount() int64 { + if x != nil { + return x.QuarantineCount + } + return 0 +} + +// ExecuteDistributionTask keeps TMS credentials and database writes on the +// management plane. The repository-node worker only triggers the fenced task +// over mTLS; target nodes still pull the immutable revision from its public URL. +type ExecuteDistributionTaskRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + WorkerId string `protobuf:"bytes,2,opt,name=worker_id,json=workerId,proto3" json:"worker_id,omitempty"` + FencingToken uint64 `protobuf:"varint,3,opt,name=fencing_token,json=fencingToken,proto3" json:"fencing_token,omitempty"` + StorageNodeId string `protobuf:"bytes,4,opt,name=storage_node_id,json=storageNodeId,proto3" json:"storage_node_id,omitempty"` +} + +func (x *ExecuteDistributionTaskRequest) Reset() { + *x = ExecuteDistributionTaskRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repositorynode_repository_node_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ExecuteDistributionTaskRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecuteDistributionTaskRequest) ProtoMessage() {} + +func (x *ExecuteDistributionTaskRequest) ProtoReflect() protoreflect.Message { + mi := &file_repositorynode_repository_node_proto_msgTypes[15] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExecuteDistributionTaskRequest.ProtoReflect.Descriptor instead. +func (*ExecuteDistributionTaskRequest) Descriptor() ([]byte, []int) { + return file_repositorynode_repository_node_proto_rawDescGZIP(), []int{15} +} + +func (x *ExecuteDistributionTaskRequest) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *ExecuteDistributionTaskRequest) GetWorkerId() string { + if x != nil { + return x.WorkerId + } + return "" +} + +func (x *ExecuteDistributionTaskRequest) GetFencingToken() uint64 { + if x != nil { + return x.FencingToken + } + return 0 +} + +func (x *ExecuteDistributionTaskRequest) GetStorageNodeId() string { + if x != nil { + return x.StorageNodeId + } + return "" +} + +type ExecuteDistributionTaskResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + DistributionId string `protobuf:"bytes,1,opt,name=distribution_id,json=distributionId,proto3" json:"distribution_id,omitempty"` + TmsTaskId string `protobuf:"bytes,2,opt,name=tms_task_id,json=tmsTaskId,proto3" json:"tms_task_id,omitempty"` + Status repository.RepositoryTaskStatus `protobuf:"varint,3,opt,name=status,proto3,enum=trpc.ocmanager.repository.RepositoryTaskStatus" json:"status,omitempty"` + ErrorCode string `protobuf:"bytes,4,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"` + ErrorMessage string `protobuf:"bytes,5,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` +} + +func (x *ExecuteDistributionTaskResponse) Reset() { + *x = ExecuteDistributionTaskResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repositorynode_repository_node_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ExecuteDistributionTaskResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecuteDistributionTaskResponse) ProtoMessage() {} + +func (x *ExecuteDistributionTaskResponse) ProtoReflect() protoreflect.Message { + mi := &file_repositorynode_repository_node_proto_msgTypes[16] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExecuteDistributionTaskResponse.ProtoReflect.Descriptor instead. +func (*ExecuteDistributionTaskResponse) Descriptor() ([]byte, []int) { + return file_repositorynode_repository_node_proto_rawDescGZIP(), []int{16} +} + +func (x *ExecuteDistributionTaskResponse) GetDistributionId() string { + if x != nil { + return x.DistributionId + } + return "" +} + +func (x *ExecuteDistributionTaskResponse) GetTmsTaskId() string { + if x != nil { + return x.TmsTaskId + } + return "" +} + +func (x *ExecuteDistributionTaskResponse) GetStatus() repository.RepositoryTaskStatus { + if x != nil { + return x.Status + } + return repository.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_UNSPECIFIED +} + +func (x *ExecuteDistributionTaskResponse) GetErrorCode() string { + if x != nil { + return x.ErrorCode + } + return "" +} + +func (x *ExecuteDistributionTaskResponse) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +type PrepareTaskCommitRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + WorkerId string `protobuf:"bytes,2,opt,name=worker_id,json=workerId,proto3" json:"worker_id,omitempty"` + FencingToken uint64 `protobuf:"varint,3,opt,name=fencing_token,json=fencingToken,proto3" json:"fencing_token,omitempty"` + RevisionId string `protobuf:"bytes,4,opt,name=revision_id,json=revisionId,proto3" json:"revision_id,omitempty"` + ManifestSha256 string `protobuf:"bytes,5,opt,name=manifest_sha256,json=manifestSha256,proto3" json:"manifest_sha256,omitempty"` + ResultObjectId string `protobuf:"bytes,6,opt,name=result_object_id,json=resultObjectId,proto3" json:"result_object_id,omitempty"` + ResultObjectSha256 string `protobuf:"bytes,7,opt,name=result_object_sha256,json=resultObjectSha256,proto3" json:"result_object_sha256,omitempty"` + PackageCount int64 `protobuf:"varint,8,opt,name=package_count,json=packageCount,proto3" json:"package_count,omitempty"` + TotalBytes int64 `protobuf:"varint,9,opt,name=total_bytes,json=totalBytes,proto3" json:"total_bytes,omitempty"` + StorageNodeId string `protobuf:"bytes,10,opt,name=storage_node_id,json=storageNodeId,proto3" json:"storage_node_id,omitempty"` + CompletionStatus repository.RepositoryTaskStatus `protobuf:"varint,11,opt,name=completion_status,json=completionStatus,proto3,enum=trpc.ocmanager.repository.RepositoryTaskStatus" json:"completion_status,omitempty"` + ErrorCode string `protobuf:"bytes,12,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"` + ErrorMessage string `protobuf:"bytes,13,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` +} + +func (x *PrepareTaskCommitRequest) Reset() { + *x = PrepareTaskCommitRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repositorynode_repository_node_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PrepareTaskCommitRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PrepareTaskCommitRequest) ProtoMessage() {} + +func (x *PrepareTaskCommitRequest) ProtoReflect() protoreflect.Message { + mi := &file_repositorynode_repository_node_proto_msgTypes[17] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PrepareTaskCommitRequest.ProtoReflect.Descriptor instead. +func (*PrepareTaskCommitRequest) Descriptor() ([]byte, []int) { + return file_repositorynode_repository_node_proto_rawDescGZIP(), []int{17} +} + +func (x *PrepareTaskCommitRequest) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *PrepareTaskCommitRequest) GetWorkerId() string { + if x != nil { + return x.WorkerId + } + return "" +} + +func (x *PrepareTaskCommitRequest) GetFencingToken() uint64 { + if x != nil { + return x.FencingToken + } + return 0 +} + +func (x *PrepareTaskCommitRequest) GetRevisionId() string { + if x != nil { + return x.RevisionId + } + return "" +} + +func (x *PrepareTaskCommitRequest) GetManifestSha256() string { + if x != nil { + return x.ManifestSha256 + } + return "" +} + +func (x *PrepareTaskCommitRequest) GetResultObjectId() string { + if x != nil { + return x.ResultObjectId + } + return "" +} + +func (x *PrepareTaskCommitRequest) GetResultObjectSha256() string { + if x != nil { + return x.ResultObjectSha256 + } + return "" +} + +func (x *PrepareTaskCommitRequest) GetPackageCount() int64 { + if x != nil { + return x.PackageCount + } + return 0 +} + +func (x *PrepareTaskCommitRequest) GetTotalBytes() int64 { + if x != nil { + return x.TotalBytes + } + return 0 +} + +func (x *PrepareTaskCommitRequest) GetStorageNodeId() string { + if x != nil { + return x.StorageNodeId + } + return "" +} + +func (x *PrepareTaskCommitRequest) GetCompletionStatus() repository.RepositoryTaskStatus { + if x != nil { + return x.CompletionStatus + } + return repository.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_UNSPECIFIED +} + +func (x *PrepareTaskCommitRequest) GetErrorCode() string { + if x != nil { + return x.ErrorCode + } + return "" +} + +func (x *PrepareTaskCommitRequest) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +type PrepareTaskCommitResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CommitToken string `protobuf:"bytes,1,opt,name=commit_token,json=commitToken,proto3" json:"commit_token,omitempty"` + RevisionId string `protobuf:"bytes,2,opt,name=revision_id,json=revisionId,proto3" json:"revision_id,omitempty"` + ManifestSha256 string `protobuf:"bytes,3,opt,name=manifest_sha256,json=manifestSha256,proto3" json:"manifest_sha256,omitempty"` + State string `protobuf:"bytes,4,opt,name=state,proto3" json:"state,omitempty"` +} + +func (x *PrepareTaskCommitResponse) Reset() { + *x = PrepareTaskCommitResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repositorynode_repository_node_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PrepareTaskCommitResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PrepareTaskCommitResponse) ProtoMessage() {} + +func (x *PrepareTaskCommitResponse) ProtoReflect() protoreflect.Message { + mi := &file_repositorynode_repository_node_proto_msgTypes[18] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PrepareTaskCommitResponse.ProtoReflect.Descriptor instead. +func (*PrepareTaskCommitResponse) Descriptor() ([]byte, []int) { + return file_repositorynode_repository_node_proto_rawDescGZIP(), []int{18} +} + +func (x *PrepareTaskCommitResponse) GetCommitToken() string { + if x != nil { + return x.CommitToken + } + return "" +} + +func (x *PrepareTaskCommitResponse) GetRevisionId() string { + if x != nil { + return x.RevisionId + } + return "" +} + +func (x *PrepareTaskCommitResponse) GetManifestSha256() string { + if x != nil { + return x.ManifestSha256 + } + return "" +} + +func (x *PrepareTaskCommitResponse) GetState() string { + if x != nil { + return x.State + } + return "" +} + +type ConfirmTaskCommitRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + WorkerId string `protobuf:"bytes,2,opt,name=worker_id,json=workerId,proto3" json:"worker_id,omitempty"` + FencingToken uint64 `protobuf:"varint,3,opt,name=fencing_token,json=fencingToken,proto3" json:"fencing_token,omitempty"` + CommitToken string `protobuf:"bytes,4,opt,name=commit_token,json=commitToken,proto3" json:"commit_token,omitempty"` + StorageNodeId string `protobuf:"bytes,5,opt,name=storage_node_id,json=storageNodeId,proto3" json:"storage_node_id,omitempty"` +} + +func (x *ConfirmTaskCommitRequest) Reset() { + *x = ConfirmTaskCommitRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repositorynode_repository_node_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ConfirmTaskCommitRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConfirmTaskCommitRequest) ProtoMessage() {} + +func (x *ConfirmTaskCommitRequest) ProtoReflect() protoreflect.Message { + mi := &file_repositorynode_repository_node_proto_msgTypes[19] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConfirmTaskCommitRequest.ProtoReflect.Descriptor instead. +func (*ConfirmTaskCommitRequest) Descriptor() ([]byte, []int) { + return file_repositorynode_repository_node_proto_rawDescGZIP(), []int{19} +} + +func (x *ConfirmTaskCommitRequest) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *ConfirmTaskCommitRequest) GetWorkerId() string { + if x != nil { + return x.WorkerId + } + return "" +} + +func (x *ConfirmTaskCommitRequest) GetFencingToken() uint64 { + if x != nil { + return x.FencingToken + } + return 0 +} + +func (x *ConfirmTaskCommitRequest) GetCommitToken() string { + if x != nil { + return x.CommitToken + } + return "" +} + +func (x *ConfirmTaskCommitRequest) GetStorageNodeId() string { + if x != nil { + return x.StorageNodeId + } + return "" +} + +var File_repositorynode_repository_node_proto protoreflect.FileDescriptor + +var file_repositorynode_repository_node_proto_rawDesc = []byte{ + 0x0a, 0x24, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x6e, 0x6f, 0x64, 0x65, + 0x2f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x6e, 0x6f, 0x64, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1d, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x6e, 0x6f, 0x64, 0x65, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2f, + 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x1a, 0x0e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x22, 0xfe, 0x08, 0x0a, 0x14, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x4e, 0x6f, + 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x0f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x0d, 0x73, + 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x30, 0x0a, 0x0f, + 0x66, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x18, 0x20, 0x52, 0x0e, + 0x66, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2e, + 0x0a, 0x0e, 0x63, 0x61, 0x70, 0x61, 0x63, 0x69, 0x74, 0x79, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, + 0x0d, 0x63, 0x61, 0x70, 0x61, 0x63, 0x69, 0x74, 0x79, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x26, + 0x0a, 0x0a, 0x66, 0x72, 0x65, 0x65, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x03, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x22, 0x02, 0x28, 0x00, 0x52, 0x09, 0x66, 0x72, 0x65, + 0x65, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x0b, 0x66, 0x72, 0x65, 0x65, 0x5f, 0x69, + 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xfa, 0x42, 0x04, + 0x22, 0x02, 0x28, 0x00, 0x52, 0x0a, 0x66, 0x72, 0x65, 0x65, 0x49, 0x6e, 0x6f, 0x64, 0x65, 0x73, + 0x12, 0x6a, 0x0a, 0x0d, 0x74, 0x6f, 0x6f, 0x6c, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x45, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, + 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, + 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x54, 0x6f, 0x6f, + 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0c, + 0x74, 0x6f, 0x6f, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x3b, 0x0a, 0x0b, + 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x6f, + 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x41, 0x74, 0x12, 0x26, 0x0a, 0x0a, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, + 0x42, 0x04, 0x72, 0x02, 0x18, 0x40, 0x52, 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, + 0x65, 0x12, 0x2d, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0x18, + 0x80, 0x10, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x12, 0x24, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0x18, 0xff, 0x01, 0x52, 0x08, 0x68, 0x6f, + 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x26, 0x0a, 0x0a, 0x69, 0x70, 0x5f, 0x61, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, + 0x02, 0x18, 0x40, 0x52, 0x09, 0x69, 0x70, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x2b, + 0x0a, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x18, 0x0c, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x18, 0x20, 0x52, 0x0c, 0x61, + 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x12, 0x21, 0x0a, 0x07, 0x6f, + 0x73, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, + 0x05, 0x72, 0x03, 0x18, 0x80, 0x01, 0x52, 0x06, 0x6f, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x26, + 0x0a, 0x0a, 0x6f, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0e, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x18, 0x40, 0x52, 0x09, 0x6f, 0x73, 0x56, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2c, 0x0a, 0x0d, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, + 0x42, 0x04, 0x72, 0x02, 0x18, 0x40, 0x52, 0x0c, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x5b, 0x0a, 0x11, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x5f, 0x75, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x2e, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, + 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x6e, 0x6f, 0x64, 0x65, 0x2e, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, + 0x10, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x73, 0x61, 0x67, 0x65, + 0x73, 0x12, 0x3a, 0x0a, 0x19, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, + 0x75, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x18, 0x11, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x17, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x55, 0x73, 0x61, 0x67, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x6e, 0x0a, + 0x15, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, + 0x67, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x12, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x4b, 0x65, 0x79, 0x49, + 0x6e, 0x76, 0x65, 0x6e, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x13, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x41, 0x0a, + 0x1d, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, + 0x67, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x18, 0x13, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x1a, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x53, 0x69, + 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x4b, 0x65, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, + 0x1a, 0x3f, 0x0a, 0x11, 0x54, 0x6f, 0x6f, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, + 0x45, 0x6e, 0x74, 0x72, 0x79, 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, 0x3a, 0x02, 0x38, + 0x01, 0x22, 0xf5, 0x02, 0x0a, 0x1b, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x53, 0x69, + 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x4b, 0x65, 0x79, 0x49, 0x6e, 0x76, 0x65, 0x6e, 0x74, 0x6f, 0x72, + 0x79, 0x12, 0x23, 0x0a, 0x07, 0x6b, 0x65, 0x79, 0x5f, 0x72, 0x65, 0x66, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0x80, 0x01, 0x52, 0x06, + 0x6b, 0x65, 0x79, 0x52, 0x65, 0x66, 0x12, 0x2b, 0x0a, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, + 0x70, 0x72, 0x69, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, + 0x72, 0x04, 0x10, 0x28, 0x18, 0x40, 0x52, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, + 0x69, 0x6e, 0x74, 0x12, 0x32, 0x0a, 0x0f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, + 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xfa, 0x42, + 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0xff, 0x01, 0x52, 0x0d, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, + 0x4b, 0x65, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2e, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0x18, 0x80, 0x10, 0x52, 0x0c, 0x70, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x4b, 0x65, 0x79, 0x55, 0x72, 0x6c, 0x12, 0x21, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, + 0x18, 0x20, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x40, 0x0a, 0x0e, 0x6b, 0x65, + 0x79, 0x5f, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0c, + 0x6b, 0x65, 0x79, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x3b, 0x0a, 0x0b, + 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x6f, + 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x41, 0x74, 0x22, 0x8c, 0x02, 0x0a, 0x0f, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x2e, 0x0a, + 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, + 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, 0x12, 0x2c, 0x0a, + 0x0d, 0x6c, 0x6f, 0x67, 0x69, 0x63, 0x61, 0x6c, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x22, 0x02, 0x28, 0x00, 0x52, 0x0c, 0x6c, + 0x6f, 0x67, 0x69, 0x63, 0x61, 0x6c, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x0e, 0x70, + 0x68, 0x79, 0x73, 0x69, 0x63, 0x61, 0x6c, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x03, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x22, 0x02, 0x28, 0x00, 0x52, 0x0d, 0x70, 0x68, + 0x79, 0x73, 0x69, 0x63, 0x61, 0x6c, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x0e, 0x70, + 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x18, 0x20, 0x52, 0x0d, 0x70, 0x75, + 0x62, 0x6c, 0x69, 0x73, 0x68, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x3b, 0x0a, 0x0b, 0x6f, + 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x6f, 0x62, + 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x41, 0x74, 0x22, 0xad, 0x01, 0x0a, 0x15, 0x48, 0x65, 0x61, + 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x74, 0x69, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x54, 0x69, 0x6d, 0x65, 0x12, + 0x34, 0x0a, 0x16, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x68, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, + 0x74, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x14, 0x6e, 0x65, 0x78, 0x74, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x53, 0x65, + 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x65, 0x6e, + 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x6e, 0x6f, 0x64, + 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0xdb, 0x02, 0x0a, 0x14, 0x42, 0x6f, 0x6f, + 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x2e, 0x0a, 0x0d, 0x65, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, + 0x24, 0x18, 0x24, 0x52, 0x0c, 0x65, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x49, + 0x64, 0x12, 0x31, 0x0a, 0x0f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x5f, 0x74, 0x61, 0x73, + 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, + 0x04, 0x10, 0x24, 0x18, 0x24, 0x52, 0x0d, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x54, 0x61, + 0x73, 0x6b, 0x49, 0x64, 0x12, 0x20, 0x0a, 0x0c, 0x74, 0x6d, 0x73, 0x5f, 0x61, 0x67, 0x65, 0x6e, + 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x74, 0x6d, 0x73, 0x41, + 0x67, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x33, 0x0a, 0x0f, 0x62, 0x6f, 0x6f, 0x74, 0x73, 0x74, + 0x72, 0x61, 0x70, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x0a, 0xfa, 0x42, 0x07, 0x72, 0x05, 0x10, 0x20, 0x18, 0x80, 0x01, 0x52, 0x0e, 0x62, 0x6f, 0x6f, + 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x24, 0x0a, 0x07, 0x63, + 0x73, 0x72, 0x5f, 0x70, 0x65, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x0b, 0xfa, 0x42, + 0x08, 0x7a, 0x06, 0x10, 0x40, 0x18, 0x80, 0x80, 0x02, 0x52, 0x06, 0x63, 0x73, 0x72, 0x50, 0x65, + 0x6d, 0x12, 0x34, 0x0a, 0x10, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, + 0x72, 0x04, 0x10, 0x24, 0x18, 0x24, 0x52, 0x0f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x2d, 0x0a, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, + 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, + 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x20, 0x52, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, + 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x22, 0x76, 0x0a, 0x1b, 0x52, 0x65, 0x6e, 0x65, 0x77, 0x4e, + 0x6f, 0x64, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x0f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, + 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, + 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x24, 0x18, 0x24, 0x52, 0x0d, 0x73, 0x74, 0x6f, 0x72, 0x61, + 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x07, 0x63, 0x73, 0x72, 0x5f, + 0x70, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x0b, 0xfa, 0x42, 0x08, 0x7a, 0x06, + 0x10, 0x40, 0x18, 0x80, 0x80, 0x02, 0x52, 0x06, 0x63, 0x73, 0x72, 0x50, 0x65, 0x6d, 0x22, 0xd3, + 0x02, 0x0a, 0x17, 0x4e, 0x6f, 0x64, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x0f, 0x73, 0x74, + 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, + 0x49, 0x64, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x5f, 0x70, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x63, 0x65, 0x72, + 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x65, 0x6d, 0x12, 0x2c, 0x0a, 0x12, 0x63, + 0x61, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x65, + 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x10, 0x63, 0x61, 0x43, 0x65, 0x72, 0x74, 0x69, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x65, 0x6d, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x65, 0x72, + 0x69, 0x61, 0x6c, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x20, + 0x0a, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, + 0x12, 0x39, 0x0a, 0x0a, 0x6e, 0x6f, 0x74, 0x5f, 0x62, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x09, 0x6e, 0x6f, 0x74, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x12, 0x37, 0x0a, 0x09, 0x6e, + 0x6f, 0x74, 0x5f, 0x61, 0x66, 0x74, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x08, 0x6e, 0x6f, 0x74, 0x41, + 0x66, 0x74, 0x65, 0x72, 0x22, 0x9f, 0x01, 0x0a, 0x10, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x54, 0x61, + 0x73, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x0f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x0d, 0x73, + 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x27, 0x0a, 0x09, + 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x0a, 0xfa, 0x42, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0x80, 0x01, 0x52, 0x08, 0x77, 0x6f, 0x72, + 0x6b, 0x65, 0x72, 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x0d, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x5f, 0x73, + 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x42, 0x0a, 0xfa, 0x42, + 0x07, 0x1a, 0x05, 0x18, 0xac, 0x02, 0x28, 0x05, 0x52, 0x0c, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x53, + 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x22, 0xa1, 0x06, 0x0a, 0x12, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x53, 0x70, 0x65, 0x63, 0x12, 0x17, 0x0a, + 0x07, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, 0x12, 0x26, 0x0a, 0x0f, 0x73, + 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, + 0x65, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x66, 0x6c, 0x69, 0x67, 0x68, 0x74, + 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x72, 0x65, 0x66, 0x6c, + 0x69, 0x67, 0x68, 0x74, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x4a, 0x0a, 0x09, 0x74, 0x61, 0x73, 0x6b, 0x5f, + 0x74, 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2d, 0x2e, 0x74, 0x72, 0x70, + 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x54, 0x61, 0x73, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x52, 0x08, 0x74, 0x61, 0x73, 0x6b, 0x54, + 0x79, 0x70, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x65, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x5f, 0x74, + 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x66, 0x65, 0x6e, 0x63, + 0x69, 0x6e, 0x67, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x44, 0x0a, 0x10, 0x6c, 0x65, 0x61, 0x73, + 0x65, 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61, 0x74, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, + 0x6c, 0x65, 0x61, 0x73, 0x65, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x21, + 0x0a, 0x0c, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x09, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x4a, 0x73, 0x6f, + 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x5f, 0x6b, 0x65, 0x79, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x4b, + 0x65, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, + 0x72, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, + 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x70, 0x67, 0x5f, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x67, 0x70, 0x67, 0x50, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x2c, 0x0a, 0x12, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, + 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x10, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x52, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x49, 0x64, 0x12, 0x53, 0x0a, 0x08, 0x67, 0x70, 0x67, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, + 0x0e, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x47, 0x50, 0x47, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, + 0x07, 0x67, 0x70, 0x67, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x6c, 0x0a, 0x10, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x18, 0x0f, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x41, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x6e, 0x6f, + 0x64, 0x65, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x66, 0x65, + 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x0f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x53, + 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x12, 0x32, 0x0a, 0x10, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, + 0x68, 0x5f, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0x18, 0x80, 0x10, 0x52, 0x0e, 0x70, 0x75, 0x62, 0x6c, + 0x69, 0x73, 0x68, 0x42, 0x61, 0x73, 0x65, 0x55, 0x72, 0x6c, 0x22, 0xda, 0x01, 0x0a, 0x22, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, + 0x65, 0x12, 0x23, 0x0a, 0x07, 0x6b, 0x65, 0x79, 0x5f, 0x72, 0x65, 0x66, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0x80, 0x01, 0x52, 0x06, + 0x6b, 0x65, 0x79, 0x52, 0x65, 0x66, 0x12, 0x2b, 0x0a, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, + 0x70, 0x72, 0x69, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, + 0x72, 0x04, 0x10, 0x28, 0x18, 0x40, 0x52, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, + 0x69, 0x6e, 0x74, 0x12, 0x32, 0x0a, 0x0f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, + 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xfa, 0x42, + 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0xff, 0x01, 0x52, 0x0d, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, + 0x4b, 0x65, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2e, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0x18, 0x80, 0x10, 0x52, 0x0c, 0x70, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x4b, 0x65, 0x79, 0x55, 0x72, 0x6c, 0x22, 0x9e, 0x01, 0x0a, 0x19, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x47, 0x50, 0x47, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x66, 0x65, + 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x20, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x08, 0x18, 0x10, + 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x2b, 0x0a, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, + 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, + 0x06, 0x72, 0x04, 0x10, 0x28, 0x18, 0x40, 0x52, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, + 0x72, 0x69, 0x6e, 0x74, 0x12, 0x32, 0x0a, 0x0f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, + 0x65, 0x79, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xfa, + 0x42, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0x80, 0x04, 0x52, 0x0d, 0x70, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x4b, 0x65, 0x79, 0x50, 0x61, 0x74, 0x68, 0x22, 0x5a, 0x0a, 0x11, 0x43, 0x6c, 0x61, 0x69, + 0x6d, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, + 0x04, 0x74, 0x61, 0x73, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x74, 0x72, + 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x52, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, + 0x74, 0x61, 0x73, 0x6b, 0x22, 0xcf, 0x02, 0x0a, 0x14, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, + 0x61, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, + 0x07, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, + 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, 0x49, + 0x64, 0x12, 0x27, 0x0a, 0x09, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0x80, 0x01, + 0x52, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x65, + 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x0c, 0x66, 0x65, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, + 0x2f, 0x0a, 0x0d, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x1a, 0x05, 0x18, 0xac, 0x02, + 0x28, 0x05, 0x52, 0x0c, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, + 0x12, 0x34, 0x0a, 0x10, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x70, 0x65, 0x72, + 0x63, 0x65, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x1a, + 0x04, 0x18, 0x64, 0x28, 0x00, 0x52, 0x0f, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x50, + 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x12, 0x2b, 0x0a, 0x0c, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, + 0x74, 0x5f, 0x73, 0x74, 0x65, 0x70, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, + 0x05, 0x72, 0x03, 0x18, 0x80, 0x01, 0x52, 0x0b, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x53, + 0x74, 0x65, 0x70, 0x12, 0x31, 0x0a, 0x0f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x6e, + 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, + 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x0d, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, + 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x22, 0x88, 0x01, 0x0a, 0x15, 0x48, 0x65, 0x61, 0x72, 0x74, + 0x62, 0x65, 0x61, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x44, 0x0a, 0x10, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, + 0x73, 0x5f, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x45, 0x78, 0x70, + 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, + 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x0f, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, + 0x64, 0x22, 0x84, 0x04, 0x0a, 0x18, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x54, 0x61, 0x73, 0x6b, + 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, + 0x0a, 0x07, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, + 0x49, 0x64, 0x12, 0x27, 0x0a, 0x09, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0x80, + 0x01, 0x52, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x66, + 0x65, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x0c, 0x66, 0x65, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x12, 0x31, 0x0a, 0x0f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x6f, 0x64, 0x65, + 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, + 0x10, 0x01, 0x18, 0x40, 0x52, 0x0d, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, + 0x65, 0x49, 0x64, 0x12, 0x47, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x2f, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x28, 0x0a, 0x0a, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x09, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x2d, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, + 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, + 0x42, 0x05, 0x72, 0x03, 0x18, 0x80, 0x10, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x32, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x5f, + 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0x18, 0x80, 0x04, 0x52, 0x0e, 0x72, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x39, 0x0a, 0x14, 0x72, 0x65, 0x73, + 0x75, 0x6c, 0x74, 0x5f, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x73, 0x68, 0x61, 0x32, 0x35, + 0x36, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x18, 0x40, + 0x52, 0x12, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x68, + 0x61, 0x32, 0x35, 0x36, 0x12, 0x32, 0x0a, 0x10, 0x71, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x69, + 0x6e, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, + 0xfa, 0x42, 0x04, 0x22, 0x02, 0x28, 0x00, 0x52, 0x0f, 0x71, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, + 0x69, 0x6e, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xc5, 0x01, 0x0a, 0x1e, 0x45, 0x78, 0x65, + 0x63, 0x75, 0x74, 0x65, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, + 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x07, 0x74, + 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, + 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x12, + 0x27, 0x0a, 0x09, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0x80, 0x01, 0x52, 0x08, + 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x65, 0x6e, 0x63, + 0x69, 0x6e, 0x67, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x0c, 0x66, 0x65, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x31, 0x0a, + 0x0f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, + 0x40, 0x52, 0x0d, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x64, + 0x22, 0xf7, 0x01, 0x0a, 0x1f, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x44, 0x69, 0x73, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x64, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x64, + 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1e, 0x0a, + 0x0b, 0x74, 0x6d, 0x73, 0x5f, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x74, 0x6d, 0x73, 0x54, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x12, 0x47, 0x0a, + 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2f, 0x2e, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, + 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, + 0x63, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x9d, 0x05, 0x0a, 0x18, 0x50, + 0x72, 0x65, 0x70, 0x61, 0x72, 0x65, 0x54, 0x61, 0x73, 0x6b, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x07, 0x74, 0x61, 0x73, 0x6b, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, + 0x01, 0x18, 0x40, 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x12, 0x27, 0x0a, 0x09, 0x77, + 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, + 0xfa, 0x42, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0x80, 0x01, 0x52, 0x08, 0x77, 0x6f, 0x72, 0x6b, + 0x65, 0x72, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x65, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x5f, + 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x66, 0x65, 0x6e, + 0x63, 0x69, 0x6e, 0x67, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x2a, 0x0a, 0x0b, 0x72, 0x65, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, + 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x0a, 0x72, 0x65, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x31, 0x0a, 0x0f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, + 0x74, 0x5f, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, + 0xfa, 0x42, 0x05, 0x72, 0x03, 0x98, 0x01, 0x40, 0x52, 0x0e, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, + 0x73, 0x74, 0x53, 0x68, 0x61, 0x32, 0x35, 0x36, 0x12, 0x34, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x75, + 0x6c, 0x74, 0x5f, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0x80, 0x04, 0x52, 0x0e, + 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x3a, + 0x0a, 0x14, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x5f, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, + 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, + 0x05, 0x72, 0x03, 0x98, 0x01, 0x40, 0x52, 0x12, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x4f, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x53, 0x68, 0x61, 0x32, 0x35, 0x36, 0x12, 0x2c, 0x0a, 0x0d, 0x70, 0x61, + 0x63, 0x6b, 0x61, 0x67, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x03, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x22, 0x02, 0x28, 0x00, 0x52, 0x0c, 0x70, 0x61, 0x63, 0x6b, + 0x61, 0x67, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x28, 0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x61, + 0x6c, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xfa, + 0x42, 0x04, 0x22, 0x02, 0x28, 0x00, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x79, 0x74, + 0x65, 0x73, 0x12, 0x31, 0x0a, 0x0f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x6f, + 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, + 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x0d, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, + 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x5c, 0x0a, 0x11, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x2f, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x61, 0x73, 0x6b, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x52, 0x10, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x12, 0x26, 0x0a, 0x0a, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x64, + 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x18, 0x40, + 0x52, 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x2d, 0x0a, 0x0d, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x0d, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0x18, 0x80, 0x10, 0x52, 0x0c, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x9e, 0x01, 0x0a, 0x19, 0x50, + 0x72, 0x65, 0x70, 0x61, 0x72, 0x65, 0x54, 0x61, 0x73, 0x6b, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6d, 0x6d, + 0x69, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x72, + 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0a, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x27, 0x0a, 0x0f, + 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x5f, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x53, + 0x68, 0x61, 0x32, 0x35, 0x36, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, 0xec, 0x01, 0x0a, 0x18, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x54, 0x61, 0x73, 0x6b, 0x43, 0x6f, 0x6d, 0x6d, 0x69, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x07, 0x74, 0x61, 0x73, 0x6b, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, + 0x10, 0x01, 0x18, 0x40, 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x12, 0x27, 0x0a, 0x09, + 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x0a, 0xfa, 0x42, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0x80, 0x01, 0x52, 0x08, 0x77, 0x6f, 0x72, + 0x6b, 0x65, 0x72, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x65, 0x6e, 0x63, 0x69, 0x6e, 0x67, + 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x66, 0x65, + 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x2b, 0x0a, 0x0c, 0x63, 0x6f, + 0x6d, 0x6d, 0x69, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0x98, 0x01, 0x40, 0x52, 0x0b, 0x63, 0x6f, 0x6d, 0x6d, + 0x69, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x31, 0x0a, 0x0f, 0x73, 0x74, 0x6f, 0x72, 0x61, + 0x67, 0x65, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x09, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x0d, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x32, 0x84, 0x08, 0x0a, 0x1d, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x7a, 0x0a, 0x0d, + 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x33, 0x2e, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x48, 0x65, + 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x6e, 0x6f, + 0x64, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x4e, 0x6f, 0x64, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6e, 0x0a, 0x09, 0x43, 0x6c, 0x61, 0x69, + 0x6d, 0x54, 0x61, 0x73, 0x6b, 0x12, 0x2f, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x54, 0x61, 0x73, 0x6b, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x54, 0x61, 0x73, 0x6b, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7a, 0x0a, 0x0d, 0x48, 0x65, 0x61, 0x72, + 0x74, 0x62, 0x65, 0x61, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x12, 0x33, 0x2e, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, + 0x65, 0x61, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, + 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, + 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x48, + 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x64, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x54, 0x61, + 0x73, 0x6b, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x12, 0x37, 0x2e, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, + 0x54, 0x61, 0x73, 0x6b, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x98, 0x01, 0x0a, 0x17, 0x45, + 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, + 0x6f, 0x6e, 0x54, 0x61, 0x73, 0x6b, 0x12, 0x3d, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x44, 0x69, + 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3e, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x44, 0x69, 0x73, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x86, 0x01, 0x0a, 0x11, 0x50, 0x72, 0x65, 0x70, 0x61, 0x72, + 0x65, 0x54, 0x61, 0x73, 0x6b, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x12, 0x37, 0x2e, 0x74, 0x72, + 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x50, 0x72, 0x65, 0x70, + 0x61, 0x72, 0x65, 0x54, 0x61, 0x73, 0x6b, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x38, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x50, 0x72, 0x65, 0x70, 0x61, 0x72, 0x65, 0x54, 0x61, 0x73, 0x6b, + 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x64, + 0x0a, 0x11, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x54, 0x61, 0x73, 0x6b, 0x43, 0x6f, 0x6d, + 0x6d, 0x69, 0x74, 0x12, 0x37, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x6e, + 0x6f, 0x64, 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x54, 0x61, 0x73, 0x6b, 0x43, + 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x12, 0x8a, 0x01, 0x0a, 0x14, 0x52, 0x65, 0x6e, 0x65, 0x77, 0x4e, 0x6f, + 0x64, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x3a, 0x2e, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x52, 0x65, + 0x6e, 0x65, 0x77, 0x4e, 0x6f, 0x64, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x36, 0x2e, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x43, 0x65, + 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x32, 0x9e, 0x01, 0x0a, 0x1e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x4e, 0x6f, 0x64, 0x65, 0x42, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x12, 0x7c, 0x0a, 0x0d, 0x42, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, + 0x70, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x33, 0x2e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x6f, 0x63, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x42, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x4e, + 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x36, 0x2e, 0x74, 0x72, 0x70, + 0x63, 0x2e, 0x6f, 0x63, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x72, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x43, + 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x42, 0x49, 0x5a, 0x47, 0x67, 0x69, 0x74, 0x65, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x4f, 0x70, 0x65, 0x6e, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x4f, 0x53, 0x2f, 0x6f, 0x63, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2f, 0x62, 0x61, + 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x6e, 0x6f, 0x64, 0x65, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_repositorynode_repository_node_proto_rawDescOnce sync.Once + file_repositorynode_repository_node_proto_rawDescData = file_repositorynode_repository_node_proto_rawDesc +) + +func file_repositorynode_repository_node_proto_rawDescGZIP() []byte { + file_repositorynode_repository_node_proto_rawDescOnce.Do(func() { + file_repositorynode_repository_node_proto_rawDescData = protoimpl.X.CompressGZIP(file_repositorynode_repository_node_proto_rawDescData) + }) + return file_repositorynode_repository_node_proto_rawDescData +} + +var file_repositorynode_repository_node_proto_msgTypes = make([]protoimpl.MessageInfo, 21) +var file_repositorynode_repository_node_proto_goTypes = []interface{}{ + (*HeartbeatNodeRequest)(nil), // 0: trpc.ocmanager.repositorynode.HeartbeatNodeRequest + (*MetadataSigningKeyInventory)(nil), // 1: trpc.ocmanager.repositorynode.MetadataSigningKeyInventory + (*RepositoryUsage)(nil), // 2: trpc.ocmanager.repositorynode.RepositoryUsage + (*HeartbeatNodeResponse)(nil), // 3: trpc.ocmanager.repositorynode.HeartbeatNodeResponse + (*BootstrapNodeRequest)(nil), // 4: trpc.ocmanager.repositorynode.BootstrapNodeRequest + (*RenewNodeCertificateRequest)(nil), // 5: trpc.ocmanager.repositorynode.RenewNodeCertificateRequest + (*NodeCertificateResponse)(nil), // 6: trpc.ocmanager.repositorynode.NodeCertificateResponse + (*ClaimTaskRequest)(nil), // 7: trpc.ocmanager.repositorynode.ClaimTaskRequest + (*RepositoryTaskSpec)(nil), // 8: trpc.ocmanager.repositorynode.RepositoryTaskSpec + (*RepositoryMetadataSigningReference)(nil), // 9: trpc.ocmanager.repositorynode.RepositoryMetadataSigningReference + (*RepositoryGPGKeyReference)(nil), // 10: trpc.ocmanager.repositorynode.RepositoryGPGKeyReference + (*ClaimTaskResponse)(nil), // 11: trpc.ocmanager.repositorynode.ClaimTaskResponse + (*HeartbeatTaskRequest)(nil), // 12: trpc.ocmanager.repositorynode.HeartbeatTaskRequest + (*HeartbeatTaskResponse)(nil), // 13: trpc.ocmanager.repositorynode.HeartbeatTaskResponse + (*ReportTaskFailureRequest)(nil), // 14: trpc.ocmanager.repositorynode.ReportTaskFailureRequest + (*ExecuteDistributionTaskRequest)(nil), // 15: trpc.ocmanager.repositorynode.ExecuteDistributionTaskRequest + (*ExecuteDistributionTaskResponse)(nil), // 16: trpc.ocmanager.repositorynode.ExecuteDistributionTaskResponse + (*PrepareTaskCommitRequest)(nil), // 17: trpc.ocmanager.repositorynode.PrepareTaskCommitRequest + (*PrepareTaskCommitResponse)(nil), // 18: trpc.ocmanager.repositorynode.PrepareTaskCommitResponse + (*ConfirmTaskCommitRequest)(nil), // 19: trpc.ocmanager.repositorynode.ConfirmTaskCommitRequest + nil, // 20: trpc.ocmanager.repositorynode.HeartbeatNodeRequest.ToolVersionsEntry + (*timestamppb.Timestamp)(nil), // 21: google.protobuf.Timestamp + (repository.RepositoryTaskType)(0), // 22: trpc.ocmanager.repository.RepositoryTaskType + (repository.RepositoryTaskStatus)(0), // 23: trpc.ocmanager.repository.RepositoryTaskStatus + (*emptypb.Empty)(nil), // 24: google.protobuf.Empty +} +var file_repositorynode_repository_node_proto_depIdxs = []int32{ + 20, // 0: trpc.ocmanager.repositorynode.HeartbeatNodeRequest.tool_versions:type_name -> trpc.ocmanager.repositorynode.HeartbeatNodeRequest.ToolVersionsEntry + 21, // 1: trpc.ocmanager.repositorynode.HeartbeatNodeRequest.observed_at:type_name -> google.protobuf.Timestamp + 2, // 2: trpc.ocmanager.repositorynode.HeartbeatNodeRequest.repository_usages:type_name -> trpc.ocmanager.repositorynode.RepositoryUsage + 1, // 3: trpc.ocmanager.repositorynode.HeartbeatNodeRequest.metadata_signing_keys:type_name -> trpc.ocmanager.repositorynode.MetadataSigningKeyInventory + 21, // 4: trpc.ocmanager.repositorynode.MetadataSigningKeyInventory.key_created_at:type_name -> google.protobuf.Timestamp + 21, // 5: trpc.ocmanager.repositorynode.MetadataSigningKeyInventory.observed_at:type_name -> google.protobuf.Timestamp + 21, // 6: trpc.ocmanager.repositorynode.RepositoryUsage.observed_at:type_name -> google.protobuf.Timestamp + 21, // 7: trpc.ocmanager.repositorynode.HeartbeatNodeResponse.server_time:type_name -> google.protobuf.Timestamp + 21, // 8: trpc.ocmanager.repositorynode.NodeCertificateResponse.not_before:type_name -> google.protobuf.Timestamp + 21, // 9: trpc.ocmanager.repositorynode.NodeCertificateResponse.not_after:type_name -> google.protobuf.Timestamp + 22, // 10: trpc.ocmanager.repositorynode.RepositoryTaskSpec.task_type:type_name -> trpc.ocmanager.repository.RepositoryTaskType + 21, // 11: trpc.ocmanager.repositorynode.RepositoryTaskSpec.lease_expires_at:type_name -> google.protobuf.Timestamp + 10, // 12: trpc.ocmanager.repositorynode.RepositoryTaskSpec.gpg_keys:type_name -> trpc.ocmanager.repositorynode.RepositoryGPGKeyReference + 9, // 13: trpc.ocmanager.repositorynode.RepositoryTaskSpec.metadata_signing:type_name -> trpc.ocmanager.repositorynode.RepositoryMetadataSigningReference + 8, // 14: trpc.ocmanager.repositorynode.ClaimTaskResponse.task:type_name -> trpc.ocmanager.repositorynode.RepositoryTaskSpec + 21, // 15: trpc.ocmanager.repositorynode.HeartbeatTaskResponse.lease_expires_at:type_name -> google.protobuf.Timestamp + 23, // 16: trpc.ocmanager.repositorynode.ReportTaskFailureRequest.status:type_name -> trpc.ocmanager.repository.RepositoryTaskStatus + 23, // 17: trpc.ocmanager.repositorynode.ExecuteDistributionTaskResponse.status:type_name -> trpc.ocmanager.repository.RepositoryTaskStatus + 23, // 18: trpc.ocmanager.repositorynode.PrepareTaskCommitRequest.completion_status:type_name -> trpc.ocmanager.repository.RepositoryTaskStatus + 0, // 19: trpc.ocmanager.repositorynode.RepositoryNodeInternalService.HeartbeatNode:input_type -> trpc.ocmanager.repositorynode.HeartbeatNodeRequest + 7, // 20: trpc.ocmanager.repositorynode.RepositoryNodeInternalService.ClaimTask:input_type -> trpc.ocmanager.repositorynode.ClaimTaskRequest + 12, // 21: trpc.ocmanager.repositorynode.RepositoryNodeInternalService.HeartbeatTask:input_type -> trpc.ocmanager.repositorynode.HeartbeatTaskRequest + 14, // 22: trpc.ocmanager.repositorynode.RepositoryNodeInternalService.ReportTaskFailure:input_type -> trpc.ocmanager.repositorynode.ReportTaskFailureRequest + 15, // 23: trpc.ocmanager.repositorynode.RepositoryNodeInternalService.ExecuteDistributionTask:input_type -> trpc.ocmanager.repositorynode.ExecuteDistributionTaskRequest + 17, // 24: trpc.ocmanager.repositorynode.RepositoryNodeInternalService.PrepareTaskCommit:input_type -> trpc.ocmanager.repositorynode.PrepareTaskCommitRequest + 19, // 25: trpc.ocmanager.repositorynode.RepositoryNodeInternalService.ConfirmTaskCommit:input_type -> trpc.ocmanager.repositorynode.ConfirmTaskCommitRequest + 5, // 26: trpc.ocmanager.repositorynode.RepositoryNodeInternalService.RenewNodeCertificate:input_type -> trpc.ocmanager.repositorynode.RenewNodeCertificateRequest + 4, // 27: trpc.ocmanager.repositorynode.RepositoryNodeBootstrapService.BootstrapNode:input_type -> trpc.ocmanager.repositorynode.BootstrapNodeRequest + 3, // 28: trpc.ocmanager.repositorynode.RepositoryNodeInternalService.HeartbeatNode:output_type -> trpc.ocmanager.repositorynode.HeartbeatNodeResponse + 11, // 29: trpc.ocmanager.repositorynode.RepositoryNodeInternalService.ClaimTask:output_type -> trpc.ocmanager.repositorynode.ClaimTaskResponse + 13, // 30: trpc.ocmanager.repositorynode.RepositoryNodeInternalService.HeartbeatTask:output_type -> trpc.ocmanager.repositorynode.HeartbeatTaskResponse + 24, // 31: trpc.ocmanager.repositorynode.RepositoryNodeInternalService.ReportTaskFailure:output_type -> google.protobuf.Empty + 16, // 32: trpc.ocmanager.repositorynode.RepositoryNodeInternalService.ExecuteDistributionTask:output_type -> trpc.ocmanager.repositorynode.ExecuteDistributionTaskResponse + 18, // 33: trpc.ocmanager.repositorynode.RepositoryNodeInternalService.PrepareTaskCommit:output_type -> trpc.ocmanager.repositorynode.PrepareTaskCommitResponse + 24, // 34: trpc.ocmanager.repositorynode.RepositoryNodeInternalService.ConfirmTaskCommit:output_type -> google.protobuf.Empty + 6, // 35: trpc.ocmanager.repositorynode.RepositoryNodeInternalService.RenewNodeCertificate:output_type -> trpc.ocmanager.repositorynode.NodeCertificateResponse + 6, // 36: trpc.ocmanager.repositorynode.RepositoryNodeBootstrapService.BootstrapNode:output_type -> trpc.ocmanager.repositorynode.NodeCertificateResponse + 28, // [28:37] is the sub-list for method output_type + 19, // [19:28] is the sub-list for method input_type + 19, // [19:19] is the sub-list for extension type_name + 19, // [19:19] is the sub-list for extension extendee + 0, // [0:19] is the sub-list for field type_name +} + +func init() { file_repositorynode_repository_node_proto_init() } +func file_repositorynode_repository_node_proto_init() { + if File_repositorynode_repository_node_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_repositorynode_repository_node_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HeartbeatNodeRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repositorynode_repository_node_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MetadataSigningKeyInventory); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repositorynode_repository_node_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryUsage); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repositorynode_repository_node_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HeartbeatNodeResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repositorynode_repository_node_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*BootstrapNodeRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repositorynode_repository_node_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RenewNodeCertificateRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repositorynode_repository_node_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*NodeCertificateResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repositorynode_repository_node_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ClaimTaskRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repositorynode_repository_node_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryTaskSpec); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repositorynode_repository_node_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryMetadataSigningReference); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repositorynode_repository_node_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RepositoryGPGKeyReference); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repositorynode_repository_node_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ClaimTaskResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repositorynode_repository_node_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HeartbeatTaskRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repositorynode_repository_node_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HeartbeatTaskResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repositorynode_repository_node_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ReportTaskFailureRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repositorynode_repository_node_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ExecuteDistributionTaskRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repositorynode_repository_node_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ExecuteDistributionTaskResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repositorynode_repository_node_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PrepareTaskCommitRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repositorynode_repository_node_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PrepareTaskCommitResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repositorynode_repository_node_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ConfirmTaskCommitRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_repositorynode_repository_node_proto_rawDesc, + NumEnums: 0, + NumMessages: 21, + NumExtensions: 0, + NumServices: 2, + }, + GoTypes: file_repositorynode_repository_node_proto_goTypes, + DependencyIndexes: file_repositorynode_repository_node_proto_depIdxs, + MessageInfos: file_repositorynode_repository_node_proto_msgTypes, + }.Build() + File_repositorynode_repository_node_proto = out.File + file_repositorynode_repository_node_proto_rawDesc = nil + file_repositorynode_repository_node_proto_goTypes = nil + file_repositorynode_repository_node_proto_depIdxs = nil +} diff --git a/manager/backend/protocol/repositorynode/repository_node.proto b/manager/backend/protocol/repositorynode/repository_node.proto new file mode 100644 index 0000000000000000000000000000000000000000..1c333ee1f6a0677f43318a8335ba1c8c8b012840 --- /dev/null +++ b/manager/backend/protocol/repositorynode/repository_node.proto @@ -0,0 +1,221 @@ +syntax = "proto3"; + +package trpc.ocmanager.repositorynode; + +option go_package = "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repositorynode"; + +import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; +import "repository/repository.proto"; +import "validate.proto"; + +// RepositoryNodeInternalService is exposed only on the mTLS internal listener. +// Messages contain metadata and object references, never RPM bytes or credentials. +service RepositoryNodeInternalService { + rpc HeartbeatNode(HeartbeatNodeRequest) returns (HeartbeatNodeResponse); + rpc ClaimTask(ClaimTaskRequest) returns (ClaimTaskResponse); + rpc HeartbeatTask(HeartbeatTaskRequest) returns (HeartbeatTaskResponse); + rpc ReportTaskFailure(ReportTaskFailureRequest) returns (google.protobuf.Empty); + rpc ExecuteDistributionTask(ExecuteDistributionTaskRequest) returns (ExecuteDistributionTaskResponse); + rpc PrepareTaskCommit(PrepareTaskCommitRequest) returns (PrepareTaskCommitResponse); + rpc ConfirmTaskCommit(ConfirmTaskCommitRequest) returns (google.protobuf.Empty); + rpc RenewNodeCertificate(RenewNodeCertificateRequest) returns (NodeCertificateResponse); +} + +// RepositoryNodeBootstrapService is served on a dedicated TLS listener. It +// deliberately does not request a client certificate because the caller has +// not established its node identity yet; authorization is the one-time token. +service RepositoryNodeBootstrapService { + rpc BootstrapNode(BootstrapNodeRequest) returns (NodeCertificateResponse); +} + +message HeartbeatNodeRequest { + string storage_node_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + string filesystem_type = 2 [(validate.rules).string = {max_len: 32}]; + int64 capacity_bytes = 3 [(validate.rules).int64 = {gt: 0}]; + int64 free_bytes = 4 [(validate.rules).int64 = {gte: 0}]; + int64 free_inodes = 5 [(validate.rules).int64 = {gte: 0}]; + map tool_versions = 6; + google.protobuf.Timestamp observed_at = 7; + string error_code = 8 [(validate.rules).string = {max_len: 64}]; + string error_message = 9 [(validate.rules).string = {max_len: 2048}]; + string hostname = 10 [(validate.rules).string = {max_len: 255}]; + string ip_address = 11 [(validate.rules).string = {max_len: 64}]; + string architecture = 12 [(validate.rules).string = {max_len: 32}]; + string os_name = 13 [(validate.rules).string = {max_len: 128}]; + string os_version = 14 [(validate.rules).string = {max_len: 64}]; + string agent_version = 15 [(validate.rules).string = {max_len: 64}]; + repeated RepositoryUsage repository_usages = 16; + bool repository_usage_snapshot = 17; + repeated MetadataSigningKeyInventory metadata_signing_keys = 18; + bool metadata_signing_key_snapshot = 19; +} + +message MetadataSigningKeyInventory { + string key_ref = 1 [(validate.rules).string = {min_len: 1, max_len: 128}]; + string fingerprint = 2 [(validate.rules).string = {min_len: 40, max_len: 64}]; + string public_key_name = 3 [(validate.rules).string = {min_len: 1, max_len: 255}]; + string public_key_url = 4 [(validate.rules).string = {max_len: 2048}]; + string status = 5 [(validate.rules).string = {min_len: 1, max_len: 32}]; + google.protobuf.Timestamp key_created_at = 6; + google.protobuf.Timestamp observed_at = 7; +} + +message RepositoryUsage { + string repository_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + int64 logical_bytes = 2 [(validate.rules).int64 = {gte: 0}]; + int64 physical_bytes = 3 [(validate.rules).int64 = {gte: 0}]; + string publish_status = 4 [(validate.rules).string = {max_len: 32}]; + google.protobuf.Timestamp observed_at = 5; +} + +message HeartbeatNodeResponse { + google.protobuf.Timestamp server_time = 1; + int32 next_heartbeat_seconds = 2; + bool node_enabled = 3; +} + +message BootstrapNodeRequest { + string enrollment_id = 1 [(validate.rules).string = {min_len: 36, max_len: 36}]; + string install_task_id = 2 [(validate.rules).string = {min_len: 36, max_len: 36}]; + uint64 tms_agent_id = 3; + string bootstrap_token = 4 [(validate.rules).string = {min_len: 32, max_len: 128}]; + bytes csr_pem = 5 [(validate.rules).bytes = {min_len: 64, max_len: 32768}]; + string authorization_id = 6 [(validate.rules).string = {min_len: 36, max_len: 36}]; + string architecture = 7 [(validate.rules).string = {min_len: 1, max_len: 32}]; +} + +message RenewNodeCertificateRequest { + string storage_node_id = 1 [(validate.rules).string = {min_len: 36, max_len: 36}]; + bytes csr_pem = 2 [(validate.rules).bytes = {min_len: 64, max_len: 32768}]; +} + +message NodeCertificateResponse { + string storage_node_id = 1; + bytes certificate_pem = 2; + bytes ca_certificate_pem = 3; + string serial_number = 4; + string fingerprint = 5; + google.protobuf.Timestamp not_before = 6; + google.protobuf.Timestamp not_after = 7; +} + +message ClaimTaskRequest { + string storage_node_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + string worker_id = 2 [(validate.rules).string = {min_len: 1, max_len: 128}]; + int32 lease_seconds = 3 [(validate.rules).int32 = {gte: 5, lte: 300}]; +} + +message RepositoryTaskSpec { + string task_id = 1; + string repository_id = 2; + string storage_node_id = 3; + string preflight_id = 4; + string revision_id = 5; + trpc.ocmanager.repository.RepositoryTaskType task_type = 6; + uint64 fencing_token = 7; + google.protobuf.Timestamp lease_expires_at = 8; + string payload_json = 9; + string publish_key = 10; + string architecture = 11; + string gpg_policy = 12; + string active_revision_id = 13; + repeated RepositoryGPGKeyReference gpg_keys = 14; + RepositoryMetadataSigningReference metadata_signing = 15; + string publish_base_url = 16 [(validate.rules).string = {max_len: 2048}]; +} + +message RepositoryMetadataSigningReference { + string key_ref = 1 [(validate.rules).string = {min_len: 1, max_len: 128}]; + string fingerprint = 2 [(validate.rules).string = {min_len: 40, max_len: 64}]; + string public_key_name = 3 [(validate.rules).string = {min_len: 1, max_len: 255}]; + string public_key_url = 4 [(validate.rules).string = {max_len: 2048}]; +} + +message RepositoryGPGKeyReference { + string key_id = 1 [(validate.rules).string = {min_len: 8, max_len: 16}]; + string fingerprint = 2 [(validate.rules).string = {min_len: 40, max_len: 64}]; + string public_key_path = 3 [(validate.rules).string = {min_len: 1, max_len: 512}]; +} + +message ClaimTaskResponse { RepositoryTaskSpec task = 1; } + +message HeartbeatTaskRequest { + string task_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + string worker_id = 2 [(validate.rules).string = {min_len: 1, max_len: 128}]; + uint64 fencing_token = 3; + int32 lease_seconds = 4 [(validate.rules).int32 = {gte: 5, lte: 300}]; + int32 progress_percent = 5 [(validate.rules).int32 = {gte: 0, lte: 100}]; + string current_step = 6 [(validate.rules).string = {max_len: 128}]; + string storage_node_id = 7 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} + +message HeartbeatTaskResponse { + google.protobuf.Timestamp lease_expires_at = 1; + bool cancel_requested = 2; +} + +// ReportTaskFailure closes a failed or canceled lease without publishing a revision. +// It is separate from the successful prepare/activate/confirm path so a failed +// data-plane execution can never mutate the active repository revision. +message ReportTaskFailureRequest { + string task_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + string worker_id = 2 [(validate.rules).string = {min_len: 1, max_len: 128}]; + uint64 fencing_token = 3; + string storage_node_id = 4 [(validate.rules).string = {min_len: 1, max_len: 64}]; + trpc.ocmanager.repository.RepositoryTaskStatus status = 5; + string error_code = 6 [(validate.rules).string = {min_len: 1, max_len: 64}]; + string error_message = 7 [(validate.rules).string = {max_len: 2048}]; + string result_object_id = 8 [(validate.rules).string = {max_len: 512}]; + string result_object_sha256 = 9 [(validate.rules).string = {max_len: 64}]; + int64 quarantine_count = 10 [(validate.rules).int64 = {gte: 0}]; +} + +// ExecuteDistributionTask keeps TMS credentials and database writes on the +// management plane. The repository-node worker only triggers the fenced task +// over mTLS; target nodes still pull the immutable revision from its public URL. +message ExecuteDistributionTaskRequest { + string task_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + string worker_id = 2 [(validate.rules).string = {min_len: 1, max_len: 128}]; + uint64 fencing_token = 3; + string storage_node_id = 4 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} + +message ExecuteDistributionTaskResponse { + string distribution_id = 1; + string tms_task_id = 2; + trpc.ocmanager.repository.RepositoryTaskStatus status = 3; + string error_code = 4; + string error_message = 5; +} + +message PrepareTaskCommitRequest { + string task_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + string worker_id = 2 [(validate.rules).string = {min_len: 1, max_len: 128}]; + uint64 fencing_token = 3; + string revision_id = 4 [(validate.rules).string = {min_len: 1, max_len: 64}]; + string manifest_sha256 = 5 [(validate.rules).string = {len: 64}]; + string result_object_id = 6 [(validate.rules).string = {min_len: 1, max_len: 512}]; + string result_object_sha256 = 7 [(validate.rules).string = {len: 64}]; + int64 package_count = 8 [(validate.rules).int64 = {gte: 0}]; + int64 total_bytes = 9 [(validate.rules).int64 = {gte: 0}]; + string storage_node_id = 10 [(validate.rules).string = {min_len: 1, max_len: 64}]; + trpc.ocmanager.repository.RepositoryTaskStatus completion_status = 11; + string error_code = 12 [(validate.rules).string = {max_len: 64}]; + string error_message = 13 [(validate.rules).string = {max_len: 2048}]; +} + +message PrepareTaskCommitResponse { + string commit_token = 1; + string revision_id = 2; + string manifest_sha256 = 3; + string state = 4; +} + +message ConfirmTaskCommitRequest { + string task_id = 1 [(validate.rules).string = {min_len: 1, max_len: 64}]; + string worker_id = 2 [(validate.rules).string = {min_len: 1, max_len: 128}]; + uint64 fencing_token = 3; + string commit_token = 4 [(validate.rules).string = {len: 64}]; + string storage_node_id = 5 [(validate.rules).string = {min_len: 1, max_len: 64}]; +} diff --git a/manager/backend/protocol/repositorynode/repository_node.trpc.go b/manager/backend/protocol/repositorynode/repository_node.trpc.go new file mode 100644 index 0000000000000000000000000000000000000000..966d67a92a51a59b7e2d7566dab7008b24ca90a2 --- /dev/null +++ b/manager/backend/protocol/repositorynode/repository_node.trpc.go @@ -0,0 +1,536 @@ +// Code generated by trpc-go/trpc-cmdline v1.0.9. DO NOT EDIT. +// source: repositorynode/repository_node.proto + +package repositorynode + +import ( + "context" + "errors" + "fmt" + + emptypb "google.golang.org/protobuf/types/known/emptypb" + _ "trpc.group/trpc-go/trpc-go" + "trpc.group/trpc-go/trpc-go/client" + "trpc.group/trpc-go/trpc-go/codec" + _ "trpc.group/trpc-go/trpc-go/http" + "trpc.group/trpc-go/trpc-go/server" +) + +// START ======================================= Server Service Definition ======================================= START + +// RepositoryNodeInternalServiceService defines service. +type RepositoryNodeInternalServiceService interface { + HeartbeatNode(ctx context.Context, req *HeartbeatNodeRequest) (*HeartbeatNodeResponse, error) + + ClaimTask(ctx context.Context, req *ClaimTaskRequest) (*ClaimTaskResponse, error) + + HeartbeatTask(ctx context.Context, req *HeartbeatTaskRequest) (*HeartbeatTaskResponse, error) + + ReportTaskFailure(ctx context.Context, req *ReportTaskFailureRequest) (*emptypb.Empty, error) + + ExecuteDistributionTask(ctx context.Context, req *ExecuteDistributionTaskRequest) (*ExecuteDistributionTaskResponse, error) + + PrepareTaskCommit(ctx context.Context, req *PrepareTaskCommitRequest) (*PrepareTaskCommitResponse, error) + + ConfirmTaskCommit(ctx context.Context, req *ConfirmTaskCommitRequest) (*emptypb.Empty, error) + + RenewNodeCertificate(ctx context.Context, req *RenewNodeCertificateRequest) (*NodeCertificateResponse, error) +} + +func RepositoryNodeInternalServiceService_HeartbeatNode_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &HeartbeatNodeRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryNodeInternalServiceService).HeartbeatNode(ctx, reqbody.(*HeartbeatNodeRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryNodeInternalServiceService_ClaimTask_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &ClaimTaskRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryNodeInternalServiceService).ClaimTask(ctx, reqbody.(*ClaimTaskRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryNodeInternalServiceService_HeartbeatTask_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &HeartbeatTaskRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryNodeInternalServiceService).HeartbeatTask(ctx, reqbody.(*HeartbeatTaskRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryNodeInternalServiceService_ReportTaskFailure_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &ReportTaskFailureRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryNodeInternalServiceService).ReportTaskFailure(ctx, reqbody.(*ReportTaskFailureRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryNodeInternalServiceService_ExecuteDistributionTask_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &ExecuteDistributionTaskRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryNodeInternalServiceService).ExecuteDistributionTask(ctx, reqbody.(*ExecuteDistributionTaskRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryNodeInternalServiceService_PrepareTaskCommit_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &PrepareTaskCommitRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryNodeInternalServiceService).PrepareTaskCommit(ctx, reqbody.(*PrepareTaskCommitRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryNodeInternalServiceService_ConfirmTaskCommit_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &ConfirmTaskCommitRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryNodeInternalServiceService).ConfirmTaskCommit(ctx, reqbody.(*ConfirmTaskCommitRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +func RepositoryNodeInternalServiceService_RenewNodeCertificate_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &RenewNodeCertificateRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryNodeInternalServiceService).RenewNodeCertificate(ctx, reqbody.(*RenewNodeCertificateRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +// RepositoryNodeInternalServiceServer_ServiceDesc descriptor for server.RegisterService. +var RepositoryNodeInternalServiceServer_ServiceDesc = server.ServiceDesc{ + ServiceName: "trpc.ocmanager.repositorynode.RepositoryNodeInternalService", + HandlerType: ((*RepositoryNodeInternalServiceService)(nil)), + Methods: []server.Method{ + { + Name: "/trpc.ocmanager.repositorynode.RepositoryNodeInternalService/HeartbeatNode", + Func: RepositoryNodeInternalServiceService_HeartbeatNode_Handler, + }, + { + Name: "/trpc.ocmanager.repositorynode.RepositoryNodeInternalService/ClaimTask", + Func: RepositoryNodeInternalServiceService_ClaimTask_Handler, + }, + { + Name: "/trpc.ocmanager.repositorynode.RepositoryNodeInternalService/HeartbeatTask", + Func: RepositoryNodeInternalServiceService_HeartbeatTask_Handler, + }, + { + Name: "/trpc.ocmanager.repositorynode.RepositoryNodeInternalService/ReportTaskFailure", + Func: RepositoryNodeInternalServiceService_ReportTaskFailure_Handler, + }, + { + Name: "/trpc.ocmanager.repositorynode.RepositoryNodeInternalService/ExecuteDistributionTask", + Func: RepositoryNodeInternalServiceService_ExecuteDistributionTask_Handler, + }, + { + Name: "/trpc.ocmanager.repositorynode.RepositoryNodeInternalService/PrepareTaskCommit", + Func: RepositoryNodeInternalServiceService_PrepareTaskCommit_Handler, + }, + { + Name: "/trpc.ocmanager.repositorynode.RepositoryNodeInternalService/ConfirmTaskCommit", + Func: RepositoryNodeInternalServiceService_ConfirmTaskCommit_Handler, + }, + { + Name: "/trpc.ocmanager.repositorynode.RepositoryNodeInternalService/RenewNodeCertificate", + Func: RepositoryNodeInternalServiceService_RenewNodeCertificate_Handler, + }, + }, +} + +// RegisterRepositoryNodeInternalServiceService registers service. +func RegisterRepositoryNodeInternalServiceService(s server.Service, svr RepositoryNodeInternalServiceService) { + if err := s.Register(&RepositoryNodeInternalServiceServer_ServiceDesc, svr); err != nil { + panic(fmt.Sprintf("RepositoryNodeInternalService register error:%v", err)) + } +} + +// RepositoryNodeBootstrapServiceService defines service. +type RepositoryNodeBootstrapServiceService interface { + BootstrapNode(ctx context.Context, req *BootstrapNodeRequest) (*NodeCertificateResponse, error) +} + +func RepositoryNodeBootstrapServiceService_BootstrapNode_Handler(svr interface{}, ctx context.Context, f server.FilterFunc) (interface{}, error) { + req := &BootstrapNodeRequest{} + filters, err := f(req) + if err != nil { + return nil, err + } + handleFunc := func(ctx context.Context, reqbody interface{}) (interface{}, error) { + return svr.(RepositoryNodeBootstrapServiceService).BootstrapNode(ctx, reqbody.(*BootstrapNodeRequest)) + } + + var rsp interface{} + rsp, err = filters.Filter(ctx, req, handleFunc) + if err != nil { + return nil, err + } + return rsp, nil +} + +// RepositoryNodeBootstrapServiceServer_ServiceDesc descriptor for server.RegisterService. +var RepositoryNodeBootstrapServiceServer_ServiceDesc = server.ServiceDesc{ + ServiceName: "trpc.ocmanager.repositorynode.RepositoryNodeBootstrapService", + HandlerType: ((*RepositoryNodeBootstrapServiceService)(nil)), + Methods: []server.Method{ + { + Name: "/trpc.ocmanager.repositorynode.RepositoryNodeBootstrapService/BootstrapNode", + Func: RepositoryNodeBootstrapServiceService_BootstrapNode_Handler, + }, + }, +} + +// RegisterRepositoryNodeBootstrapServiceService registers service. +func RegisterRepositoryNodeBootstrapServiceService(s server.Service, svr RepositoryNodeBootstrapServiceService) { + if err := s.Register(&RepositoryNodeBootstrapServiceServer_ServiceDesc, svr); err != nil { + panic(fmt.Sprintf("RepositoryNodeBootstrapService register error:%v", err)) + } +} + +// START --------------------------------- Default Unimplemented Server Service --------------------------------- START + +type UnimplementedRepositoryNodeInternalService struct{} + +func (s *UnimplementedRepositoryNodeInternalService) HeartbeatNode(ctx context.Context, req *HeartbeatNodeRequest) (*HeartbeatNodeResponse, error) { + return nil, errors.New("rpc HeartbeatNode of service RepositoryNodeInternalService is not implemented") +} +func (s *UnimplementedRepositoryNodeInternalService) ClaimTask(ctx context.Context, req *ClaimTaskRequest) (*ClaimTaskResponse, error) { + return nil, errors.New("rpc ClaimTask of service RepositoryNodeInternalService is not implemented") +} +func (s *UnimplementedRepositoryNodeInternalService) HeartbeatTask(ctx context.Context, req *HeartbeatTaskRequest) (*HeartbeatTaskResponse, error) { + return nil, errors.New("rpc HeartbeatTask of service RepositoryNodeInternalService is not implemented") +} +func (s *UnimplementedRepositoryNodeInternalService) ReportTaskFailure(ctx context.Context, req *ReportTaskFailureRequest) (*emptypb.Empty, error) { + return nil, errors.New("rpc ReportTaskFailure of service RepositoryNodeInternalService is not implemented") +} +func (s *UnimplementedRepositoryNodeInternalService) ExecuteDistributionTask(ctx context.Context, req *ExecuteDistributionTaskRequest) (*ExecuteDistributionTaskResponse, error) { + return nil, errors.New("rpc ExecuteDistributionTask of service RepositoryNodeInternalService is not implemented") +} +func (s *UnimplementedRepositoryNodeInternalService) PrepareTaskCommit(ctx context.Context, req *PrepareTaskCommitRequest) (*PrepareTaskCommitResponse, error) { + return nil, errors.New("rpc PrepareTaskCommit of service RepositoryNodeInternalService is not implemented") +} +func (s *UnimplementedRepositoryNodeInternalService) ConfirmTaskCommit(ctx context.Context, req *ConfirmTaskCommitRequest) (*emptypb.Empty, error) { + return nil, errors.New("rpc ConfirmTaskCommit of service RepositoryNodeInternalService is not implemented") +} +func (s *UnimplementedRepositoryNodeInternalService) RenewNodeCertificate(ctx context.Context, req *RenewNodeCertificateRequest) (*NodeCertificateResponse, error) { + return nil, errors.New("rpc RenewNodeCertificate of service RepositoryNodeInternalService is not implemented") +} + +type UnimplementedRepositoryNodeBootstrapService struct{} + +func (s *UnimplementedRepositoryNodeBootstrapService) BootstrapNode(ctx context.Context, req *BootstrapNodeRequest) (*NodeCertificateResponse, error) { + return nil, errors.New("rpc BootstrapNode of service RepositoryNodeBootstrapService is not implemented") +} + +// END --------------------------------- Default Unimplemented Server Service --------------------------------- END + +// END ======================================= Server Service Definition ======================================= END + +// START ======================================= Client Service Definition ======================================= START + +// RepositoryNodeInternalServiceClientProxy defines service client proxy +type RepositoryNodeInternalServiceClientProxy interface { + HeartbeatNode(ctx context.Context, req *HeartbeatNodeRequest, opts ...client.Option) (rsp *HeartbeatNodeResponse, err error) + + ClaimTask(ctx context.Context, req *ClaimTaskRequest, opts ...client.Option) (rsp *ClaimTaskResponse, err error) + + HeartbeatTask(ctx context.Context, req *HeartbeatTaskRequest, opts ...client.Option) (rsp *HeartbeatTaskResponse, err error) + + ReportTaskFailure(ctx context.Context, req *ReportTaskFailureRequest, opts ...client.Option) (rsp *emptypb.Empty, err error) + + ExecuteDistributionTask(ctx context.Context, req *ExecuteDistributionTaskRequest, opts ...client.Option) (rsp *ExecuteDistributionTaskResponse, err error) + + PrepareTaskCommit(ctx context.Context, req *PrepareTaskCommitRequest, opts ...client.Option) (rsp *PrepareTaskCommitResponse, err error) + + ConfirmTaskCommit(ctx context.Context, req *ConfirmTaskCommitRequest, opts ...client.Option) (rsp *emptypb.Empty, err error) + + RenewNodeCertificate(ctx context.Context, req *RenewNodeCertificateRequest, opts ...client.Option) (rsp *NodeCertificateResponse, err error) +} + +type RepositoryNodeInternalServiceClientProxyImpl struct { + client client.Client + opts []client.Option +} + +var NewRepositoryNodeInternalServiceClientProxy = func(opts ...client.Option) RepositoryNodeInternalServiceClientProxy { + return &RepositoryNodeInternalServiceClientProxyImpl{client: client.DefaultClient, opts: opts} +} + +func (c *RepositoryNodeInternalServiceClientProxyImpl) HeartbeatNode(ctx context.Context, req *HeartbeatNodeRequest, opts ...client.Option) (*HeartbeatNodeResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repositorynode.RepositoryNodeInternalService/HeartbeatNode") + msg.WithCalleeServiceName(RepositoryNodeInternalServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repositorynode") + msg.WithCalleeService("RepositoryNodeInternalService") + msg.WithCalleeMethod("HeartbeatNode") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &HeartbeatNodeResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryNodeInternalServiceClientProxyImpl) ClaimTask(ctx context.Context, req *ClaimTaskRequest, opts ...client.Option) (*ClaimTaskResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repositorynode.RepositoryNodeInternalService/ClaimTask") + msg.WithCalleeServiceName(RepositoryNodeInternalServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repositorynode") + msg.WithCalleeService("RepositoryNodeInternalService") + msg.WithCalleeMethod("ClaimTask") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &ClaimTaskResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryNodeInternalServiceClientProxyImpl) HeartbeatTask(ctx context.Context, req *HeartbeatTaskRequest, opts ...client.Option) (*HeartbeatTaskResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repositorynode.RepositoryNodeInternalService/HeartbeatTask") + msg.WithCalleeServiceName(RepositoryNodeInternalServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repositorynode") + msg.WithCalleeService("RepositoryNodeInternalService") + msg.WithCalleeMethod("HeartbeatTask") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &HeartbeatTaskResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryNodeInternalServiceClientProxyImpl) ReportTaskFailure(ctx context.Context, req *ReportTaskFailureRequest, opts ...client.Option) (*emptypb.Empty, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repositorynode.RepositoryNodeInternalService/ReportTaskFailure") + msg.WithCalleeServiceName(RepositoryNodeInternalServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repositorynode") + msg.WithCalleeService("RepositoryNodeInternalService") + msg.WithCalleeMethod("ReportTaskFailure") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &emptypb.Empty{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryNodeInternalServiceClientProxyImpl) ExecuteDistributionTask(ctx context.Context, req *ExecuteDistributionTaskRequest, opts ...client.Option) (*ExecuteDistributionTaskResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repositorynode.RepositoryNodeInternalService/ExecuteDistributionTask") + msg.WithCalleeServiceName(RepositoryNodeInternalServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repositorynode") + msg.WithCalleeService("RepositoryNodeInternalService") + msg.WithCalleeMethod("ExecuteDistributionTask") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &ExecuteDistributionTaskResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryNodeInternalServiceClientProxyImpl) PrepareTaskCommit(ctx context.Context, req *PrepareTaskCommitRequest, opts ...client.Option) (*PrepareTaskCommitResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repositorynode.RepositoryNodeInternalService/PrepareTaskCommit") + msg.WithCalleeServiceName(RepositoryNodeInternalServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repositorynode") + msg.WithCalleeService("RepositoryNodeInternalService") + msg.WithCalleeMethod("PrepareTaskCommit") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &PrepareTaskCommitResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryNodeInternalServiceClientProxyImpl) ConfirmTaskCommit(ctx context.Context, req *ConfirmTaskCommitRequest, opts ...client.Option) (*emptypb.Empty, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repositorynode.RepositoryNodeInternalService/ConfirmTaskCommit") + msg.WithCalleeServiceName(RepositoryNodeInternalServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repositorynode") + msg.WithCalleeService("RepositoryNodeInternalService") + msg.WithCalleeMethod("ConfirmTaskCommit") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &emptypb.Empty{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +func (c *RepositoryNodeInternalServiceClientProxyImpl) RenewNodeCertificate(ctx context.Context, req *RenewNodeCertificateRequest, opts ...client.Option) (*NodeCertificateResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repositorynode.RepositoryNodeInternalService/RenewNodeCertificate") + msg.WithCalleeServiceName(RepositoryNodeInternalServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repositorynode") + msg.WithCalleeService("RepositoryNodeInternalService") + msg.WithCalleeMethod("RenewNodeCertificate") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &NodeCertificateResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +// RepositoryNodeBootstrapServiceClientProxy defines service client proxy +type RepositoryNodeBootstrapServiceClientProxy interface { + BootstrapNode(ctx context.Context, req *BootstrapNodeRequest, opts ...client.Option) (rsp *NodeCertificateResponse, err error) +} + +type RepositoryNodeBootstrapServiceClientProxyImpl struct { + client client.Client + opts []client.Option +} + +var NewRepositoryNodeBootstrapServiceClientProxy = func(opts ...client.Option) RepositoryNodeBootstrapServiceClientProxy { + return &RepositoryNodeBootstrapServiceClientProxyImpl{client: client.DefaultClient, opts: opts} +} + +func (c *RepositoryNodeBootstrapServiceClientProxyImpl) BootstrapNode(ctx context.Context, req *BootstrapNodeRequest, opts ...client.Option) (*NodeCertificateResponse, error) { + ctx, msg := codec.WithCloneMessage(ctx) + defer codec.PutBackMessage(msg) + msg.WithClientRPCName("/trpc.ocmanager.repositorynode.RepositoryNodeBootstrapService/BootstrapNode") + msg.WithCalleeServiceName(RepositoryNodeBootstrapServiceServer_ServiceDesc.ServiceName) + msg.WithCalleeApp("ocmanager") + msg.WithCalleeServer("repositorynode") + msg.WithCalleeService("RepositoryNodeBootstrapService") + msg.WithCalleeMethod("BootstrapNode") + msg.WithSerializationType(codec.SerializationTypePB) + callopts := make([]client.Option, 0, len(c.opts)+len(opts)) + callopts = append(callopts, c.opts...) + callopts = append(callopts, opts...) + rsp := &NodeCertificateResponse{} + if err := c.client.Invoke(ctx, req, rsp, callopts...); err != nil { + return nil, err + } + return rsp, nil +} + +// END ======================================= Client Service Definition ======================================= END diff --git a/manager/backend/protocol/repositorynode/repository_node_mock.go b/manager/backend/protocol/repositorynode/repository_node_mock.go new file mode 100644 index 0000000000000000000000000000000000000000..31d74533ab8cc0c9c92134afeb23869c96b498c7 --- /dev/null +++ b/manager/backend/protocol/repositorynode/repository_node_mock.go @@ -0,0 +1,421 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: protocol/repositorynode/repository_node.trpc.go + +// Package repositorynode is a generated GoMock package. +package repositorynode + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + emptypb "google.golang.org/protobuf/types/known/emptypb" + client "trpc.group/trpc-go/trpc-go/client" +) + +// MockRepositoryNodeInternalServiceService is a mock of RepositoryNodeInternalServiceService interface. +type MockRepositoryNodeInternalServiceService struct { + ctrl *gomock.Controller + recorder *MockRepositoryNodeInternalServiceServiceMockRecorder +} + +// MockRepositoryNodeInternalServiceServiceMockRecorder is the mock recorder for MockRepositoryNodeInternalServiceService. +type MockRepositoryNodeInternalServiceServiceMockRecorder struct { + mock *MockRepositoryNodeInternalServiceService +} + +// NewMockRepositoryNodeInternalServiceService creates a new mock instance. +func NewMockRepositoryNodeInternalServiceService(ctrl *gomock.Controller) *MockRepositoryNodeInternalServiceService { + mock := &MockRepositoryNodeInternalServiceService{ctrl: ctrl} + mock.recorder = &MockRepositoryNodeInternalServiceServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepositoryNodeInternalServiceService) EXPECT() *MockRepositoryNodeInternalServiceServiceMockRecorder { + return m.recorder +} + +// ClaimTask mocks base method. +func (m *MockRepositoryNodeInternalServiceService) ClaimTask(ctx context.Context, req *ClaimTaskRequest) (*ClaimTaskResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ClaimTask", ctx, req) + ret0, _ := ret[0].(*ClaimTaskResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ClaimTask indicates an expected call of ClaimTask. +func (mr *MockRepositoryNodeInternalServiceServiceMockRecorder) ClaimTask(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClaimTask", reflect.TypeOf((*MockRepositoryNodeInternalServiceService)(nil).ClaimTask), ctx, req) +} + +// ConfirmTaskCommit mocks base method. +func (m *MockRepositoryNodeInternalServiceService) ConfirmTaskCommit(ctx context.Context, req *ConfirmTaskCommitRequest) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConfirmTaskCommit", ctx, req) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ConfirmTaskCommit indicates an expected call of ConfirmTaskCommit. +func (mr *MockRepositoryNodeInternalServiceServiceMockRecorder) ConfirmTaskCommit(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfirmTaskCommit", reflect.TypeOf((*MockRepositoryNodeInternalServiceService)(nil).ConfirmTaskCommit), ctx, req) +} + +// ExecuteDistributionTask mocks base method. +func (m *MockRepositoryNodeInternalServiceService) ExecuteDistributionTask(ctx context.Context, req *ExecuteDistributionTaskRequest) (*ExecuteDistributionTaskResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExecuteDistributionTask", ctx, req) + ret0, _ := ret[0].(*ExecuteDistributionTaskResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExecuteDistributionTask indicates an expected call of ExecuteDistributionTask. +func (mr *MockRepositoryNodeInternalServiceServiceMockRecorder) ExecuteDistributionTask(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteDistributionTask", reflect.TypeOf((*MockRepositoryNodeInternalServiceService)(nil).ExecuteDistributionTask), ctx, req) +} + +// HeartbeatNode mocks base method. +func (m *MockRepositoryNodeInternalServiceService) HeartbeatNode(ctx context.Context, req *HeartbeatNodeRequest) (*HeartbeatNodeResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HeartbeatNode", ctx, req) + ret0, _ := ret[0].(*HeartbeatNodeResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HeartbeatNode indicates an expected call of HeartbeatNode. +func (mr *MockRepositoryNodeInternalServiceServiceMockRecorder) HeartbeatNode(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HeartbeatNode", reflect.TypeOf((*MockRepositoryNodeInternalServiceService)(nil).HeartbeatNode), ctx, req) +} + +// HeartbeatTask mocks base method. +func (m *MockRepositoryNodeInternalServiceService) HeartbeatTask(ctx context.Context, req *HeartbeatTaskRequest) (*HeartbeatTaskResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HeartbeatTask", ctx, req) + ret0, _ := ret[0].(*HeartbeatTaskResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HeartbeatTask indicates an expected call of HeartbeatTask. +func (mr *MockRepositoryNodeInternalServiceServiceMockRecorder) HeartbeatTask(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HeartbeatTask", reflect.TypeOf((*MockRepositoryNodeInternalServiceService)(nil).HeartbeatTask), ctx, req) +} + +// PrepareTaskCommit mocks base method. +func (m *MockRepositoryNodeInternalServiceService) PrepareTaskCommit(ctx context.Context, req *PrepareTaskCommitRequest) (*PrepareTaskCommitResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PrepareTaskCommit", ctx, req) + ret0, _ := ret[0].(*PrepareTaskCommitResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PrepareTaskCommit indicates an expected call of PrepareTaskCommit. +func (mr *MockRepositoryNodeInternalServiceServiceMockRecorder) PrepareTaskCommit(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrepareTaskCommit", reflect.TypeOf((*MockRepositoryNodeInternalServiceService)(nil).PrepareTaskCommit), ctx, req) +} + +// RenewNodeCertificate mocks base method. +func (m *MockRepositoryNodeInternalServiceService) RenewNodeCertificate(ctx context.Context, req *RenewNodeCertificateRequest) (*NodeCertificateResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RenewNodeCertificate", ctx, req) + ret0, _ := ret[0].(*NodeCertificateResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RenewNodeCertificate indicates an expected call of RenewNodeCertificate. +func (mr *MockRepositoryNodeInternalServiceServiceMockRecorder) RenewNodeCertificate(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RenewNodeCertificate", reflect.TypeOf((*MockRepositoryNodeInternalServiceService)(nil).RenewNodeCertificate), ctx, req) +} + +// ReportTaskFailure mocks base method. +func (m *MockRepositoryNodeInternalServiceService) ReportTaskFailure(ctx context.Context, req *ReportTaskFailureRequest) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReportTaskFailure", ctx, req) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ReportTaskFailure indicates an expected call of ReportTaskFailure. +func (mr *MockRepositoryNodeInternalServiceServiceMockRecorder) ReportTaskFailure(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportTaskFailure", reflect.TypeOf((*MockRepositoryNodeInternalServiceService)(nil).ReportTaskFailure), ctx, req) +} + +// MockRepositoryNodeBootstrapServiceService is a mock of RepositoryNodeBootstrapServiceService interface. +type MockRepositoryNodeBootstrapServiceService struct { + ctrl *gomock.Controller + recorder *MockRepositoryNodeBootstrapServiceServiceMockRecorder +} + +// MockRepositoryNodeBootstrapServiceServiceMockRecorder is the mock recorder for MockRepositoryNodeBootstrapServiceService. +type MockRepositoryNodeBootstrapServiceServiceMockRecorder struct { + mock *MockRepositoryNodeBootstrapServiceService +} + +// NewMockRepositoryNodeBootstrapServiceService creates a new mock instance. +func NewMockRepositoryNodeBootstrapServiceService(ctrl *gomock.Controller) *MockRepositoryNodeBootstrapServiceService { + mock := &MockRepositoryNodeBootstrapServiceService{ctrl: ctrl} + mock.recorder = &MockRepositoryNodeBootstrapServiceServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepositoryNodeBootstrapServiceService) EXPECT() *MockRepositoryNodeBootstrapServiceServiceMockRecorder { + return m.recorder +} + +// BootstrapNode mocks base method. +func (m *MockRepositoryNodeBootstrapServiceService) BootstrapNode(ctx context.Context, req *BootstrapNodeRequest) (*NodeCertificateResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BootstrapNode", ctx, req) + ret0, _ := ret[0].(*NodeCertificateResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BootstrapNode indicates an expected call of BootstrapNode. +func (mr *MockRepositoryNodeBootstrapServiceServiceMockRecorder) BootstrapNode(ctx, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BootstrapNode", reflect.TypeOf((*MockRepositoryNodeBootstrapServiceService)(nil).BootstrapNode), ctx, req) +} + +// MockRepositoryNodeInternalServiceClientProxy is a mock of RepositoryNodeInternalServiceClientProxy interface. +type MockRepositoryNodeInternalServiceClientProxy struct { + ctrl *gomock.Controller + recorder *MockRepositoryNodeInternalServiceClientProxyMockRecorder +} + +// MockRepositoryNodeInternalServiceClientProxyMockRecorder is the mock recorder for MockRepositoryNodeInternalServiceClientProxy. +type MockRepositoryNodeInternalServiceClientProxyMockRecorder struct { + mock *MockRepositoryNodeInternalServiceClientProxy +} + +// NewMockRepositoryNodeInternalServiceClientProxy creates a new mock instance. +func NewMockRepositoryNodeInternalServiceClientProxy(ctrl *gomock.Controller) *MockRepositoryNodeInternalServiceClientProxy { + mock := &MockRepositoryNodeInternalServiceClientProxy{ctrl: ctrl} + mock.recorder = &MockRepositoryNodeInternalServiceClientProxyMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepositoryNodeInternalServiceClientProxy) EXPECT() *MockRepositoryNodeInternalServiceClientProxyMockRecorder { + return m.recorder +} + +// ClaimTask mocks base method. +func (m *MockRepositoryNodeInternalServiceClientProxy) ClaimTask(ctx context.Context, req *ClaimTaskRequest, opts ...client.Option) (*ClaimTaskResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ClaimTask", varargs...) + ret0, _ := ret[0].(*ClaimTaskResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ClaimTask indicates an expected call of ClaimTask. +func (mr *MockRepositoryNodeInternalServiceClientProxyMockRecorder) ClaimTask(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClaimTask", reflect.TypeOf((*MockRepositoryNodeInternalServiceClientProxy)(nil).ClaimTask), varargs...) +} + +// ConfirmTaskCommit mocks base method. +func (m *MockRepositoryNodeInternalServiceClientProxy) ConfirmTaskCommit(ctx context.Context, req *ConfirmTaskCommitRequest, opts ...client.Option) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ConfirmTaskCommit", varargs...) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ConfirmTaskCommit indicates an expected call of ConfirmTaskCommit. +func (mr *MockRepositoryNodeInternalServiceClientProxyMockRecorder) ConfirmTaskCommit(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfirmTaskCommit", reflect.TypeOf((*MockRepositoryNodeInternalServiceClientProxy)(nil).ConfirmTaskCommit), varargs...) +} + +// ExecuteDistributionTask mocks base method. +func (m *MockRepositoryNodeInternalServiceClientProxy) ExecuteDistributionTask(ctx context.Context, req *ExecuteDistributionTaskRequest, opts ...client.Option) (*ExecuteDistributionTaskResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ExecuteDistributionTask", varargs...) + ret0, _ := ret[0].(*ExecuteDistributionTaskResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExecuteDistributionTask indicates an expected call of ExecuteDistributionTask. +func (mr *MockRepositoryNodeInternalServiceClientProxyMockRecorder) ExecuteDistributionTask(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteDistributionTask", reflect.TypeOf((*MockRepositoryNodeInternalServiceClientProxy)(nil).ExecuteDistributionTask), varargs...) +} + +// HeartbeatNode mocks base method. +func (m *MockRepositoryNodeInternalServiceClientProxy) HeartbeatNode(ctx context.Context, req *HeartbeatNodeRequest, opts ...client.Option) (*HeartbeatNodeResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "HeartbeatNode", varargs...) + ret0, _ := ret[0].(*HeartbeatNodeResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HeartbeatNode indicates an expected call of HeartbeatNode. +func (mr *MockRepositoryNodeInternalServiceClientProxyMockRecorder) HeartbeatNode(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HeartbeatNode", reflect.TypeOf((*MockRepositoryNodeInternalServiceClientProxy)(nil).HeartbeatNode), varargs...) +} + +// HeartbeatTask mocks base method. +func (m *MockRepositoryNodeInternalServiceClientProxy) HeartbeatTask(ctx context.Context, req *HeartbeatTaskRequest, opts ...client.Option) (*HeartbeatTaskResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "HeartbeatTask", varargs...) + ret0, _ := ret[0].(*HeartbeatTaskResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HeartbeatTask indicates an expected call of HeartbeatTask. +func (mr *MockRepositoryNodeInternalServiceClientProxyMockRecorder) HeartbeatTask(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HeartbeatTask", reflect.TypeOf((*MockRepositoryNodeInternalServiceClientProxy)(nil).HeartbeatTask), varargs...) +} + +// PrepareTaskCommit mocks base method. +func (m *MockRepositoryNodeInternalServiceClientProxy) PrepareTaskCommit(ctx context.Context, req *PrepareTaskCommitRequest, opts ...client.Option) (*PrepareTaskCommitResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "PrepareTaskCommit", varargs...) + ret0, _ := ret[0].(*PrepareTaskCommitResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PrepareTaskCommit indicates an expected call of PrepareTaskCommit. +func (mr *MockRepositoryNodeInternalServiceClientProxyMockRecorder) PrepareTaskCommit(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrepareTaskCommit", reflect.TypeOf((*MockRepositoryNodeInternalServiceClientProxy)(nil).PrepareTaskCommit), varargs...) +} + +// RenewNodeCertificate mocks base method. +func (m *MockRepositoryNodeInternalServiceClientProxy) RenewNodeCertificate(ctx context.Context, req *RenewNodeCertificateRequest, opts ...client.Option) (*NodeCertificateResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "RenewNodeCertificate", varargs...) + ret0, _ := ret[0].(*NodeCertificateResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RenewNodeCertificate indicates an expected call of RenewNodeCertificate. +func (mr *MockRepositoryNodeInternalServiceClientProxyMockRecorder) RenewNodeCertificate(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RenewNodeCertificate", reflect.TypeOf((*MockRepositoryNodeInternalServiceClientProxy)(nil).RenewNodeCertificate), varargs...) +} + +// ReportTaskFailure mocks base method. +func (m *MockRepositoryNodeInternalServiceClientProxy) ReportTaskFailure(ctx context.Context, req *ReportTaskFailureRequest, opts ...client.Option) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ReportTaskFailure", varargs...) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ReportTaskFailure indicates an expected call of ReportTaskFailure. +func (mr *MockRepositoryNodeInternalServiceClientProxyMockRecorder) ReportTaskFailure(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportTaskFailure", reflect.TypeOf((*MockRepositoryNodeInternalServiceClientProxy)(nil).ReportTaskFailure), varargs...) +} + +// MockRepositoryNodeBootstrapServiceClientProxy is a mock of RepositoryNodeBootstrapServiceClientProxy interface. +type MockRepositoryNodeBootstrapServiceClientProxy struct { + ctrl *gomock.Controller + recorder *MockRepositoryNodeBootstrapServiceClientProxyMockRecorder +} + +// MockRepositoryNodeBootstrapServiceClientProxyMockRecorder is the mock recorder for MockRepositoryNodeBootstrapServiceClientProxy. +type MockRepositoryNodeBootstrapServiceClientProxyMockRecorder struct { + mock *MockRepositoryNodeBootstrapServiceClientProxy +} + +// NewMockRepositoryNodeBootstrapServiceClientProxy creates a new mock instance. +func NewMockRepositoryNodeBootstrapServiceClientProxy(ctrl *gomock.Controller) *MockRepositoryNodeBootstrapServiceClientProxy { + mock := &MockRepositoryNodeBootstrapServiceClientProxy{ctrl: ctrl} + mock.recorder = &MockRepositoryNodeBootstrapServiceClientProxyMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepositoryNodeBootstrapServiceClientProxy) EXPECT() *MockRepositoryNodeBootstrapServiceClientProxyMockRecorder { + return m.recorder +} + +// BootstrapNode mocks base method. +func (m *MockRepositoryNodeBootstrapServiceClientProxy) BootstrapNode(ctx context.Context, req *BootstrapNodeRequest, opts ...client.Option) (*NodeCertificateResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "BootstrapNode", varargs...) + ret0, _ := ret[0].(*NodeCertificateResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BootstrapNode indicates an expected call of BootstrapNode. +func (mr *MockRepositoryNodeBootstrapServiceClientProxyMockRecorder) BootstrapNode(ctx, req interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BootstrapNode", reflect.TypeOf((*MockRepositoryNodeBootstrapServiceClientProxy)(nil).BootstrapNode), varargs...) +} diff --git a/manager/backend/services/footstone/application/application.go b/manager/backend/services/footstone/application/application.go index 25b6c7b5c0199a5a5aa6dce27e1853566a419920..33003b28c96ed862be18f440130cae31843b3291 100644 --- a/manager/backend/services/footstone/application/application.go +++ b/manager/backend/services/footstone/application/application.go @@ -2,8 +2,10 @@ package application import ( + "context" "errors" "fmt" + "time" "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" ftpb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/footstone" @@ -15,10 +17,27 @@ import ( "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/footstoneService" "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryAIService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryContentService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryDistributionService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryInventoryService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryNodeService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryPackageInstallService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryScheduleService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryTaskService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryUpstreamService" "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/userService" "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" cache "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/distributed-cache" "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/lock" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/nodeclient" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/nodeinstaller" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/nodepki" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/tmsclient" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/repositoryworker" + "trpc.group/trpc-go/trpc-go/log" ) var errCreateProjectEmptyResponse = errors.New("create project failed: empty response") @@ -31,17 +50,38 @@ type instanceUpdater interface { UpdateInstanceAgent(entity.Context, *po.Instance) error } +type repositoryNodeResultReader interface { + ReadTaskResult(context.Context, *po.RepositoryStorageNode, string, string, string) ([]byte, error) +} + type application struct { - UserFavoriteSrv footstoneService.UserFavoriteService - MetricsMetaSrv footstoneService.MetricsMetaService - infraService footstoneService.FootstoneService - instanceUpdater instanceUpdater - userService userService.UserService - auditService auditService.AuditService - accessService accessService.AccessService - cacheService cacheService.CacheService - appConf *config.ApplicationConfig - Lock lock.Lock + UserFavoriteSrv footstoneService.UserFavoriteService + MetricsMetaSrv footstoneService.MetricsMetaService + infraService footstoneService.FootstoneService + instanceUpdater instanceUpdater + userService userService.UserService + auditService auditService.AuditService + accessService accessService.AccessService + cacheService cacheService.CacheService + repositorySrv repositoryService.Service + repositoryContentSrv repositoryContentService.Service + repositoryDistributionSrv repositoryDistributionService.Service + repositoryInventorySrv repositoryInventoryService.Service + repositoryNodeSrv repositoryNodeService.Service + repositoryPackageInstallSrv repositoryPackageInstallService.Service + repositoryAISrv repositoryAIService.Service + repositoryTaskSrv repositoryTaskService.Service + repositoryScheduleSrv repositoryScheduleService.Service + repositoryUpstreamSrv repositoryUpstreamService.Service + repositoryDistributionWorker *repositoryworker.Handler + repositoryNodeResult repositoryNodeResultReader + repositoryNodeInstaller *nodeinstaller.Dispatcher + repositoryTMS tmsclient.Client + appConf *config.ApplicationConfig + Lock lock.Lock + schedulerCancel context.CancelFunc + schedulerDone chan struct{} + repositoryMaintenanceDone chan struct{} } func initDependence(appConfig *config.ApplicationConfig) (dependence.DependenceAggregation, error) { @@ -86,22 +126,137 @@ func NewApplicationService(appconfig *config.ApplicationConfig, appentity *confi auditServiceIns := auditService.NewAuditService(appentity.Resource.MetaDB, appconfig) userFavoriteService := footstoneService.NewUserFavoriteService(appentity.Resource.MetaDB, appconfig) metricsMetaService := footstoneService.NewMetricsMetaService(appentity.Resource.MetaDB) + repositorySrv := repositoryService.New(appentity.Resource.MetaDB) + repositoryTaskSrv := repositoryTaskService.New(appentity.Resource.MetaDB) + repositoryUpstreamSrv := repositoryUpstreamService.New(appentity.Resource.MetaDB) + repositoryNodeSrv := repositoryNodeService.New(appentity.Resource.MetaDB) + repositoryAISrv := repositoryAIService.New(appentity.Resource.MetaDB) + repositoryConfig := appconfig.Application.Repository.WithDefaults() + if err = repositoryNodeSrv.ConfigureArtifacts(repositoryConfig.NodeArtifactRoot); err != nil { + return nil, err + } + if repositoryConfig.NodeBootstrap.Enabled { + issuer, issuerErr := nodepki.New(repositoryConfig.NodeBootstrap.SigningCACert, + repositoryConfig.NodeBootstrap.SigningCAKey, + time.Duration(repositoryConfig.NodeBootstrap.CertificateDays)*24*time.Hour) + if issuerErr != nil { + return nil, issuerErr + } + repositoryNodeSrv.ConfigureLifecycle(issuer, request.RepositoryNodeLifecyclePolicy{ + EnrollmentTTL: time.Duration(repositoryConfig.NodeBootstrap.EnrollmentTTLMinutes) * time.Minute, + CertificateTTL: time.Duration(repositoryConfig.NodeBootstrap.CertificateDays) * 24 * time.Hour, + RenewBefore: time.Duration(repositoryConfig.NodeBootstrap.RenewBeforeDays) * 24 * time.Hour, + CertificateGrace: time.Duration(repositoryConfig.NodeBootstrap.CertificateGraceHours) * time.Hour, + }) + } + repositoryNodeSrv.ConfigurePreflight(request.RepositoryPreflightPolicy{ + MaxExpandedBytes: repositoryConfig.MaxExpandedBytes, + MaxSyncBytes: repositoryConfig.MaxSyncBytes, + MaxSyncPackages: int64(repositoryConfig.MaxSyncPackages), + FilteredSyncBytes: repositoryConfig.FilteredSyncBytes, + FilteredSyncPackages: int64(repositoryConfig.FilteredSyncPackages), + MaxArchiveFiles: int64(repositoryConfig.MaxArchiveFiles), + MetadataReserve: repositoryConfig.MetadataReserve, + MinimumFreeBytes: repositoryConfig.MinimumFreeBytes, + SafetyPercent: 20, HardUsagePercent: 90, + }) + repositoryTaskSrv.ConfigurePreflight(repositoryNodeSrv) + repositoryStore, err := storage.New(appconfig.Application.Repository) + if err != nil { + return nil, err + } + repositoryContentSrv := repositoryContentService.New(appentity.Resource.MetaDB, repositorySrv, + repositoryTaskSrv, repositoryStore) + repositoryInventorySrv := repositoryInventoryService.New(appentity.Resource.MetaDB, repositorySrv, repositoryTaskSrv) + repositoryScheduleSrv := repositoryScheduleService.New(appentity.Resource.MetaDB, repositorySrv, + repositoryContentSrv, repositoryTaskSrv) + tms := tmsclient.New(appconfig.Application.Repository.TMSBaseURL, + appconfig.Application.Repository.TMSUserName) + repositoryDistributionSrv := repositoryDistributionService.New(appentity.Resource.MetaDB, repositorySrv, + repositoryTaskSrv, repositoryStore, tms) + repositoryPackageInstallSrv := repositoryPackageInstallService.New(appentity.Resource.MetaDB, repositorySrv, + repositoryContentSrv, repositoryTaskSrv, tms) + var repositoryNodeResult repositoryNodeResultReader + dataClientConfig := appconfig.Application.Repository.WithDefaults().NodeDataClient + if dataClientConfig.Enabled { + var dataClient *nodeclient.Client + dataClient, err = nodeclient.New(dataClientConfig, repositoryStore.Root()) + if err != nil { + return nil, err + } + repositoryNodeResult = dataClient + repositoryContentSrv.ConfigureRemoteUploads(repositoryNodeSrv, + &repositoryNodeUploadAdapter{client: dataClient}) + repositoryDistributionSrv.ConfigureRemotePublication() + } + repositoryDistributionWorker := repositoryworker.NewHandler(appentity.Resource.MetaDB, repositoryStore) return &application{ - UserFavoriteSrv: userFavoriteService, - MetricsMetaSrv: metricsMetaService, - infraService: infraservice, - instanceUpdater: infraservice, - userService: userService, - auditService: auditServiceIns, - cacheService: cacheService, - accessService: accessService, - appConf: appconfig, - Lock: appentity.Resource.Lock, + UserFavoriteSrv: userFavoriteService, + MetricsMetaSrv: metricsMetaService, + infraService: infraservice, + instanceUpdater: infraservice, + userService: userService, + auditService: auditServiceIns, + cacheService: cacheService, + repositorySrv: repositorySrv, + repositoryContentSrv: repositoryContentSrv, + repositoryInventorySrv: repositoryInventorySrv, + repositoryDistributionSrv: repositoryDistributionSrv, + repositoryNodeSrv: repositoryNodeSrv, + repositoryPackageInstallSrv: repositoryPackageInstallSrv, + repositoryAISrv: repositoryAISrv, + repositoryTaskSrv: repositoryTaskSrv, + repositoryScheduleSrv: repositoryScheduleSrv, + repositoryUpstreamSrv: repositoryUpstreamSrv, + repositoryDistributionWorker: repositoryDistributionWorker, + repositoryNodeResult: repositoryNodeResult, + repositoryNodeInstaller: nodeinstaller.New(tms), + repositoryTMS: tms, + accessService: accessService, + appConf: appconfig, + Lock: appentity.Resource.Lock, }, nil } // Start keeps application compatible with the service lifecycle manager. func (a *application) Start() error { + if a == nil || a.schedulerCancel != nil { + return nil + } + ctx, cancel := context.WithCancel(context.Background()) + a.schedulerCancel = cancel + a.schedulerDone = make(chan struct{}) + a.repositoryMaintenanceDone = make(chan struct{}) + go func() { + defer close(a.repositoryMaintenanceDone) + runRepositoryMaintenance(ctx, 10*time.Second, a.reconcileRepositoryMaintenance) + }() + go func() { + defer close(a.schedulerDone) + if a.repositoryScheduleSrv == nil { + return + } + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + for { + if err := a.repositoryScheduleSrv.Tick(); err != nil { + log.Errorf("repository schedule tick failed: %v", err) + } + if err := a.reconcileRepositoryNodeInstallations(ctx); err != nil { + log.Errorf("repository node installation reconciliation failed: %v", err) + } + if a.repositoryPackageInstallSrv != nil { + if err := a.repositoryPackageInstallSrv.Reconcile(ctx); err != nil { + log.Errorf("repository package installation reconciliation failed: %v", err) + } + } + select { + case <-ctx.Done(): + return + case <-ticker.C: + } + } + }() return nil } @@ -113,6 +268,16 @@ func (a *application) Stop() error { if a.auditService != nil { a.auditService.Stop() } + if a.schedulerCancel != nil { + a.schedulerCancel() + if a.schedulerDone != nil { + <-a.schedulerDone + } + if a.repositoryMaintenanceDone != nil { + <-a.repositoryMaintenanceDone + } + a.schedulerCancel = nil + } return nil } @@ -125,6 +290,36 @@ func (a *application) SyncDataSchema() error { if err = a.userService.SyncDataSchema(); err != nil { return err } + if err = a.repositorySrv.SyncTables(); err != nil { + return err + } + if err = a.repositoryTaskSrv.SyncTables(); err != nil { + return err + } + if err = a.repositoryScheduleSrv.SyncTables(); err != nil { + return err + } + if err = a.repositoryUpstreamSrv.SyncTables(); err != nil { + return err + } + if err = a.repositoryNodeSrv.SyncTables(); err != nil { + return err + } + if err = a.repositoryPackageInstallSrv.SyncTables(); err != nil { + return err + } + if err = a.repositoryAISrv.SyncTables(); err != nil { + return err + } + if err = a.repositoryContentSrv.SyncTables(); err != nil { + return err + } + if err = a.repositoryInventorySrv.SyncTables(); err != nil { + return err + } + if err = a.repositoryDistributionSrv.SyncTables(); err != nil { + return err + } return err } diff --git a/manager/backend/services/footstone/application/repository.go b/manager/backend/services/footstone/application/repository.go new file mode 100644 index 0000000000000000000000000000000000000000..3d9d4ba365331a779f33b89b0765726a248801f4 --- /dev/null +++ b/manager/backend/services/footstone/application/repository.go @@ -0,0 +1,167 @@ +package application + +import ( + "fmt" + "strings" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryService" +) + +func (a *application) CreateRepository(ctx entity.Context, req *repositorypb.CreateRepositoryRequest) ( + *po.SoftwareRepository, error, +) { + spec, err := repositorySpecFromPB(req.GetSpec()) + if err != nil { + return nil, err + } + return a.repositorySrv.Create(ctx, spec) +} + +func (a *application) UpdateRepository(ctx entity.Context, req *repositorypb.UpdateRepositoryRequest) ( + *po.SoftwareRepository, error, +) { + spec, err := repositorySpecFromPB(req.GetSpec()) + if err != nil { + return nil, err + } + return a.repositorySrv.Update(ctx, req.GetRepositoryId(), spec) +} + +func (a *application) DescribeRepository(ctx entity.Context, id string) (*po.SoftwareRepository, error) { + return a.repositorySrv.Describe(ctx, id) +} + +func (a *application) ListRepositories(ctx entity.Context, req *repositorypb.ListRepositoriesRequest) ( + []po.SoftwareRepository, request.PageResponse, error, +) { + if req == nil { + return nil, request.PageResponse{}, fmt.Errorf("request is required") + } + return a.repositorySrv.List(ctx, request.ListSoftwareRepositoriesRequest{ + ProjectID: req.GetProjectId(), + SystemProduct: req.GetSystemProduct(), + ProductVersion: req.GetProductVersion(), + Architecture: architectureFromPB(req.GetArchitecture()), + Type: typeFromPB(req.GetType()), + Keyword: req.GetKeyword(), + PageRequest: request.ToPageRequest(req.GetPageRequest()), + }) +} + +func (a *application) DeleteRepository(ctx entity.Context, req *repositorypb.DeleteRepositoryRequest) error { + if req == nil { + return fmt.Errorf("request is required") + } + return a.repositorySrv.Delete(ctx, req.GetRepositoryId(), req.GetConfirmDelete()) +} + +func (a *application) BatchApplyRepositories(ctx entity.Context, req *repositorypb.BatchApplyRepositoriesRequest) ( + []repositoryService.BatchResult, error, +) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + items := make([]request.BatchApplySoftwareRepositoryItem, 0, len(req.GetItems())) + for index, item := range req.GetItems() { + if item == nil { + return nil, fmt.Errorf("batch item %d is required", index) + } + spec, err := repositorySpecFromPB(item.GetSpec()) + if err != nil { + return nil, fmt.Errorf("batch item %d: %w", index, err) + } + items = append(items, request.BatchApplySoftwareRepositoryItem{ + RepositoryID: item.GetRepositoryId(), + Spec: spec, + }) + } + return a.repositorySrv.BatchApply(ctx, items) +} + +func repositorySpecFromPB(spec *repositorypb.RepositorySpec) (request.SoftwareRepositorySpec, error) { + if spec == nil { + return request.SoftwareRepositorySpec{}, fmt.Errorf("repository spec is required") + } + enabled := true + if spec.Enabled != nil { + enabled = spec.GetEnabled() + } + return request.SoftwareRepositorySpec{ + ProjectID: spec.GetProjectId(), + SystemProduct: spec.GetSystemProduct(), + ProductVersion: spec.GetProductVersion(), + Name: spec.GetName(), + Description: spec.GetDescription(), + Architecture: architectureFromPB(spec.GetArchitecture()), + Type: typeFromPB(spec.GetType()), + UpstreamURL: spec.GetUpstreamUrl(), + AllowUpload: spec.GetAllowUpload(), + IncludePackages: append([]string(nil), spec.GetIncludePackages()...), + ExcludePackages: append([]string(nil), spec.GetExcludePackages()...), + GPGPolicy: gpgPolicyFromPB(spec.GetGpgPolicy()), + Enabled: enabled, + PrimaryStorageNodeID: spec.GetPrimaryStorageNodeId(), + SyncMode: syncModeFromPB(spec.GetSyncMode()), + BandwidthLimitBytes: spec.GetBandwidthLimitBytes(), + MaxParallelDownloads: int(spec.GetMaxParallelDownloads()), + MetadataSigningKeyRef: spec.GetMetadataSigningKeyRef(), + }, nil +} + +func syncModeFromPB(value repositorypb.RepositorySyncMode) string { + switch value { + case repositorypb.RepositorySyncMode_REPOSITORY_SYNC_MODE_APPEND: + return request.RepositorySyncModeAppend + case repositorypb.RepositorySyncMode_REPOSITORY_SYNC_MODE_MIRROR, + repositorypb.RepositorySyncMode_REPOSITORY_SYNC_MODE_UNSPECIFIED: + return request.RepositorySyncModeMirror + default: + return "" + } +} + +func architectureFromPB(value repositorypb.RepositoryArchitecture) string { + switch value { + case repositorypb.RepositoryArchitecture_REPOSITORY_ARCHITECTURE_X86_64: + return request.RepositoryArchitectureX8664 + case repositorypb.RepositoryArchitecture_REPOSITORY_ARCHITECTURE_AARCH64: + return request.RepositoryArchitectureARM64 + case repositorypb.RepositoryArchitecture_REPOSITORY_ARCHITECTURE_NOARCH: + return request.RepositoryArchitectureNoarch + default: + return "" + } +} + +func typeFromPB(value repositorypb.RepositoryType) string { + switch value { + case repositorypb.RepositoryType_REPOSITORY_TYPE_BASELINE: + return request.RepositoryTypeBaseline + case repositorypb.RepositoryType_REPOSITORY_TYPE_UPDATE: + return request.RepositoryTypeUpdate + case repositorypb.RepositoryType_REPOSITORY_TYPE_PRIVATE: + return request.RepositoryTypePrivate + default: + return "" + } +} + +func gpgPolicyFromPB(value repositorypb.RepositoryGPGPolicy) string { + switch value { + case repositorypb.RepositoryGPGPolicy_REPOSITORY_GPG_POLICY_VERIFY_PACKAGE: + return request.RepositoryGPGVerifyPackage + case repositorypb.RepositoryGPGPolicy_REPOSITORY_GPG_POLICY_AUDIT: + return request.RepositoryGPGAudit + case repositorypb.RepositoryGPGPolicy_REPOSITORY_GPG_POLICY_ENFORCE: + return request.RepositoryGPGEnforce + case repositorypb.RepositoryGPGPolicy_REPOSITORY_GPG_POLICY_DISABLED, + repositorypb.RepositoryGPGPolicy_REPOSITORY_GPG_POLICY_UNSPECIFIED: + return request.RepositoryGPGDisabled + default: + return strings.TrimPrefix(value.String(), "REPOSITORY_GPG_POLICY_") + } +} diff --git a/manager/backend/services/footstone/application/repository_content.go b/manager/backend/services/footstone/application/repository_content.go new file mode 100644 index 0000000000000000000000000000000000000000..f56e8d4499d2d24310f10f718a9a09eb81df1937 --- /dev/null +++ b/manager/backend/services/footstone/application/repository_content.go @@ -0,0 +1,183 @@ +package application + +import ( + "fmt" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + commonpb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/common" + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryContentService" +) + +func (a *application) SyncRepository(ctx entity.Context, repositoryID string) (*po.RepositoryTask, error) { + return a.repositoryContentSrv.SyncRepository(ctx, repositoryID) +} + +func (a *application) MigrateRepository(ctx entity.Context, + req *repositorypb.MigrateRepositoryRequest, +) (*po.RepositoryTask, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + return a.repositoryContentSrv.MigrateRepository(ctx, req.GetRepositoryId(), req.GetTargetStorageNodeId()) +} + +func (a *application) GenerateRepositoryConfig(ctx entity.Context, repositoryID string, immutableRevision bool) ( + *request.GeneratedRepositoryConfig, error, +) { + return a.repositoryContentSrv.GenerateRepositoryConfig(ctx, repositoryID, immutableRevision) +} + +func (a *application) RegisterRepositoryGPGKey(ctx entity.Context, + req *repositorypb.RegisterRepositoryGPGKeyRequest, +) (*request.RepositoryGPGKey, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + return a.repositoryContentSrv.RegisterGPGKey(ctx, req.GetRepositoryId(), req.GetPublicKey()) +} + +func (a *application) ListRepositoryGPGKeys(ctx entity.Context, + req *repositorypb.ListRepositoryGPGKeysRequest, +) ([]request.RepositoryGPGKey, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + return a.repositoryContentSrv.ListGPGKeys(ctx, req.GetRepositoryId()) +} + +func (a *application) DeleteRepositoryGPGKey(ctx entity.Context, + req *repositorypb.DeleteRepositoryGPGKeyRequest, +) error { + if req == nil { + return fmt.Errorf("request is required") + } + return a.repositoryContentSrv.DeleteGPGKey(ctx, req.GetRepositoryId(), req.GetKeyId(), req.GetConfirmDelete()) +} + +func (a *application) BeginRepositoryUpload(ctx entity.Context, req *repositorypb.BeginRepositoryUploadRequest) ( + *po.RepositoryUpload, error, +) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + return a.repositoryContentSrv.BeginUpload(ctx, request.BeginRepositoryUploadRequest{ + RepositoryID: req.GetRepositoryId(), FileName: req.GetFileName(), + TotalSize: req.GetTotalSize(), ExpectedSHA256: req.GetExpectedSha256(), + ClientFingerprint: req.GetClientFingerprint(), ChunkSize: int(req.GetChunkSize()), + }) +} + +func (a *application) AppendRepositoryUploadChunk(ctx entity.Context, + req *repositorypb.AppendRepositoryUploadChunkRequest, +) (*po.RepositoryUpload, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + return a.repositoryContentSrv.AppendChunk(ctx, request.AppendRepositoryUploadChunkRequest{ + UploadID: req.GetUploadId(), Offset: req.GetOffset(), Data: req.GetData(), + }) +} + +func (a *application) PutRepositoryUploadChunk(ctx entity.Context, + req *repositorypb.PutRepositoryUploadChunkRequest, +) (*po.RepositoryUpload, *po.RepositoryUploadChunk, error) { + if req == nil { + return nil, nil, fmt.Errorf("request is required") + } + return a.repositoryContentSrv.PutChunk(ctx, request.PutRepositoryUploadChunkRequest{ + UploadID: req.GetUploadId(), ChunkIndex: int(req.GetChunkIndex()), + ChunkSHA256: req.GetChunkSha256(), Data: req.GetData(), + }) +} + +func (a *application) DescribeRepositoryUploadChunks(ctx entity.Context, uploadID string) ( + *po.RepositoryUpload, []po.RepositoryUploadChunk, []int, error, +) { + return a.repositoryContentSrv.DescribeUploadChunks(ctx, uploadID) +} + +func (a *application) PauseRepositoryUpload(ctx entity.Context, uploadID string) (*po.RepositoryUpload, error) { + return a.repositoryContentSrv.PauseUpload(ctx, uploadID) +} + +func (a *application) ResumeRepositoryUpload(ctx entity.Context, uploadID string) (*po.RepositoryUpload, error) { + return a.repositoryContentSrv.ResumeUpload(ctx, uploadID) +} + +func (a *application) CompleteRepositoryUpload(ctx entity.Context, uploadID string) ( + *po.RepositoryUpload, *po.RepositoryTask, error, +) { + return a.repositoryContentSrv.CompleteUpload(ctx, uploadID) +} + +func (a *application) AbortRepositoryUpload(ctx entity.Context, uploadID string) (*po.RepositoryUpload, error) { + return a.repositoryContentSrv.AbortUpload(ctx, uploadID) +} + +func (a *application) DescribeRepositoryUpload(ctx entity.Context, uploadID string) (*po.RepositoryUpload, error) { + return a.repositoryContentSrv.DescribeUpload(ctx, uploadID) +} + +func (a *application) ListRepositoryPackages(ctx entity.Context, req *repositorypb.ListRepositoryPackagesRequest) ( + []po.RepositoryPackage, string, request.PageResponse, error, +) { + if req == nil { + return nil, "", request.PageResponse{}, fmt.Errorf("request is required") + } + return a.repositoryContentSrv.ListPackages(ctx, request.ListRepositoryPackagesRequest{ + RepositoryID: req.GetRepositoryId(), RevisionID: req.GetRevisionId(), Name: req.GetName(), + Architecture: architectureFromPB(req.GetArchitecture()), PageRequest: pageRequest(req.GetPageRequest()), + }) +} + +func (a *application) ListRepositoryQuarantine(ctx entity.Context, + req *repositorypb.ListRepositoryQuarantineRequest, +) ([]po.RepositoryQuarantineItem, request.PageResponse, error) { + if req == nil { + return nil, request.PageResponse{}, fmt.Errorf("request is required") + } + return a.repositoryContentSrv.ListQuarantine(ctx, request.ListRepositoryQuarantineRequest{ + RepositoryID: req.GetRepositoryId(), TaskID: req.GetTaskId(), ReasonCode: req.GetReasonCode(), + PageRequest: pageRequest(req.GetPageRequest()), + }) +} + +func (a *application) DeleteRepositoryPackage(ctx entity.Context, req *repositorypb.DeleteRepositoryPackageRequest) ( + *po.RepositoryTask, error, +) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + return a.repositoryContentSrv.DeletePackage(ctx, request.DeleteRepositoryPackageRequest{ + RepositoryID: req.GetRepositoryId(), NEVRA: req.GetNevra(), + }) +} + +func (a *application) PreviewRepositoryPackageDeletion(ctx entity.Context, + req *repositorypb.PreviewRepositoryPackageDeletionRequest, +) (*repositoryContentService.PackageDeletionPreviewResult, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + return a.repositoryContentSrv.PreviewPackageDeletion(ctx, request.PreviewRepositoryPackageDeletionRequest{ + RepositoryID: req.GetRepositoryId(), NEVRAs: req.GetNevras(), + }) +} + +func (a *application) ConfirmRepositoryPackageDeletion(ctx entity.Context, + req *repositorypb.ConfirmRepositoryPackageDeletionRequest, +) (*po.RepositoryTask, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + return a.repositoryContentSrv.ConfirmPackageDeletion(ctx, request.ConfirmRepositoryPackageDeletionRequest{ + PreviewID: req.GetPreviewId(), PreviewDigest: req.GetPreviewDigest(), + }) +} + +func pageRequest(value *commonpb.PageRequest) request.PageRequest { + return request.ToPageRequest(value) +} diff --git a/manager/backend/services/footstone/application/repository_distribution.go b/manager/backend/services/footstone/application/repository_distribution.go new file mode 100644 index 0000000000000000000000000000000000000000..e1952810b64b46e56395a049ac1116a071181fc6 --- /dev/null +++ b/manager/backend/services/footstone/application/repository_distribution.go @@ -0,0 +1,37 @@ +package application + +import ( + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +func (a *application) ListRepositoryDistributionNodes(ctx entity.Context) ( + []request.RepositoryDistributionNode, error, +) { + return a.repositoryDistributionSrv.ListNodes(ctx) +} + +func (a *application) CreateRepositoryDistribution(ctx entity.Context, + req *repositorypb.CreateRepositoryDistributionRequest, +) (*po.RepositoryDistribution, *po.RepositoryTask, error) { + return a.repositoryDistributionSrv.Create(ctx, request.CreateRepositoryDistributionRequest{ + RepositoryID: req.GetRepositoryId(), RevisionID: req.GetRevisionId(), + TargetIPs: req.GetTargetIps(), TimeoutSeconds: int(req.GetTimeoutSeconds()), + }) +} + +func (a *application) DescribeRepositoryDistribution(ctx entity.Context, id string) ( + *po.RepositoryDistribution, error, +) { + return a.repositoryDistributionSrv.Describe(ctx, id) +} + +func (a *application) ListRepositoryDistributions(ctx entity.Context, + req *repositorypb.ListRepositoryDistributionsRequest, +) ([]po.RepositoryDistribution, request.PageResponse, error) { + return a.repositoryDistributionSrv.List(ctx, request.ListRepositoryDistributionsRequest{ + RepositoryID: req.GetRepositoryId(), PageRequest: request.ToPageRequest(req.GetPageRequest()), + }) +} diff --git a/manager/backend/services/footstone/application/repository_maintenance.go b/manager/backend/services/footstone/application/repository_maintenance.go new file mode 100644 index 0000000000000000000000000000000000000000..518ec877d9c1729b4477e0b0ab7a2b584b6f739b --- /dev/null +++ b/manager/backend/services/footstone/application/repository_maintenance.go @@ -0,0 +1,40 @@ +package application + +import ( + "context" + "time" + + "trpc.group/trpc-go/trpc-go/log" +) + +// Maintenance must not depend on a node heartbeat or a slow TMS reconciliation. +func (a *application) reconcileRepositoryMaintenance(ctx context.Context) { + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + if a.repositoryTaskSrv != nil { + if _, err := a.repositoryTaskSrv.RecoverExpiredContext(ctx); err != nil { + log.Errorf("repository lease recovery failed: %v", err) + } + } + if a.repositoryNodeSrv != nil { + if _, err := a.repositoryNodeSrv.MarkOfflineContext(ctx); err != nil { + log.Errorf("repository node offline reconciliation failed: %v", err) + } + } +} + +func runRepositoryMaintenance(ctx context.Context, interval time.Duration, reconcile func(context.Context)) { + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + if ctx.Err() != nil { + return + } + reconcile(ctx) + select { + case <-ctx.Done(): + return + case <-ticker.C: + } + } +} diff --git a/manager/backend/services/footstone/application/repository_maintenance_test.go b/manager/backend/services/footstone/application/repository_maintenance_test.go new file mode 100644 index 0000000000000000000000000000000000000000..992305b9d0e130fec88538f7eb1e49ccfb3185d6 --- /dev/null +++ b/manager/backend/services/footstone/application/repository_maintenance_test.go @@ -0,0 +1,70 @@ +package application + +import ( + "context" + "sync/atomic" + "testing" + "time" +) + +func TestRepositoryMaintenanceRunsWithoutHeartbeatOrSchedule(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + calls := make(chan struct{}, 3) + done := make(chan struct{}) + go func() { + defer close(done) + runRepositoryMaintenance(ctx, time.Millisecond, func(context.Context) { + select { + case calls <- struct{}{}: + default: + } + }) + }() + for index := 0; index < 2; index++ { + select { + case <-calls: + case <-time.After(time.Second): + t.Fatal("maintenance did not run independently") + } + } + cancel() + select { + case <-done: + case <-time.After(time.Second): + t.Fatal("maintenance did not stop with application context") + } +} + +func TestRepositoryMaintenanceCanceledBeforeStartDoesNotRun(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + var calls atomic.Int64 + runRepositoryMaintenance(ctx, time.Second, func(context.Context) { calls.Add(1) }) + if calls.Load() != 0 { + t.Fatal("maintenance ran after shutdown") + } +} + +func TestRepositoryMaintenanceAllowsAbsentOptionalServices(t *testing.T) { + a := &application{} + a.reconcileRepositoryMaintenance(context.Background()) +} + +func TestRepositoryMaintenanceLifecycleWithoutScheduler(t *testing.T) { + a := &application{} + if err := a.Start(); err != nil { + t.Fatal(err) + } + if a.repositoryMaintenanceDone == nil { + t.Fatal("maintenance was not started without a scheduler") + } + if err := a.Stop(); err != nil { + t.Fatal(err) + } + select { + case <-a.repositoryMaintenanceDone: + default: + t.Fatal("maintenance did not finish before Stop returned") + } +} diff --git a/manager/backend/services/footstone/application/repository_node.go b/manager/backend/services/footstone/application/repository_node.go new file mode 100644 index 0000000000000000000000000000000000000000..77d1f3acdb85b87d0764b6e5f79570c1b00accb5 --- /dev/null +++ b/manager/backend/services/footstone/application/repository_node.go @@ -0,0 +1,371 @@ +package application + +import ( + "context" + "fmt" + "os" + "strings" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryNodeService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/nodeinstaller" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/nodeprotocol" +) + +type repositoryNodeProbeReader interface { + Probe(context.Context, *po.RepositoryStorageNode) (*nodeprotocol.ProbeResponse, error) +} + +func (a *application) CreateRepositoryStorageNode(ctx entity.Context, + req *repositorypb.CreateRepositoryStorageNodeRequest, +) (*po.RepositoryStorageNode, error) { + spec, err := repositoryStorageNodeSpecFromPB(req.GetSpec()) + if err != nil { + return nil, err + } + return a.repositoryNodeSrv.Create(ctx, spec) +} + +func (a *application) UpdateRepositoryStorageNode(ctx entity.Context, + req *repositorypb.UpdateRepositoryStorageNodeRequest, +) (*po.RepositoryStorageNode, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + spec, err := repositoryStorageNodeSpecFromPB(req.GetSpec()) + if err != nil { + return nil, err + } + return a.repositoryNodeSrv.Update(ctx, req.GetStorageNodeId(), spec) +} + +func (a *application) DescribeRepositoryStorageNode(ctx entity.Context, id string) ( + *po.RepositoryStorageNode, error, +) { + return a.repositoryNodeSrv.Describe(ctx, id) +} + +func (a *application) ListRepositoryStorageNodes(ctx entity.Context, + req *repositorypb.ListRepositoryStorageNodesRequest, +) ([]po.RepositoryStorageNode, request.PageResponse, request.RepositoryStorageNodeStatistics, error) { + if req == nil { + return nil, request.PageResponse{}, request.RepositoryStorageNodeStatistics{}, fmt.Errorf("request is required") + } + return a.repositoryNodeSrv.ListWithStatistics(ctx, request.ListRepositoryStorageNodesRequest{ + ProjectID: req.GetProjectId(), Status: storageNodeStatusFromPB(req.GetStatus()), + Keyword: req.GetKeyword(), PageRequest: request.ToPageRequest(req.GetPageRequest()), + }) +} + +func (a *application) DeleteRepositoryStorageNode(ctx entity.Context, + req *repositorypb.DeleteRepositoryStorageNodeRequest, +) error { + if req == nil { + return fmt.Errorf("request is required") + } + return a.repositoryNodeSrv.Delete(ctx, req.GetStorageNodeId(), req.GetConfirmDelete()) +} + +func (a *application) ProbeRepositoryStorageNode(ctx entity.Context, id string) ( + *po.RepositoryStorageNode, *po.RepositoryPreflightRecord, error, +) { + _, _ = a.repositoryNodeSrv.MarkOffline() + node, err := a.repositoryNodeSrv.Describe(ctx, id) + if err != nil { + return nil, nil, err + } + probeClient, ok := a.repositoryNodeResult.(repositoryNodeProbeReader) + if !ok { + return nil, nil, ErrRepositoryNodeDataUnavailable + } + probe, err := probeClient.Probe(ctx, node) + if err != nil { + return nil, nil, err + } + usages := make([]request.RepositoryNodeRepositoryUsage, 0, len(probe.RepositoryUsages)) + for _, usage := range probe.RepositoryUsages { + usages = append(usages, request.RepositoryNodeRepositoryUsage{RepositoryID: usage.RepositoryID, + LogicalBytes: usage.LogicalBytes, PhysicalBytes: usage.PhysicalBytes, + PublishStatus: usage.PublishStatus, ObservedAt: usage.ObservedAt}) + } + return a.repositoryNodeSrv.RecordProbe(ctx, request.RepositoryStorageNodeHeartbeat{ + NodeID: id, Hostname: probe.Hostname, IPAddress: probe.IPAddress, Architecture: probe.Architecture, + OSName: probe.OSName, OSVersion: probe.OSVersion, AgentVersion: probe.AgentVersion, + FilesystemType: probe.FilesystemType, CapacityBytes: probe.CapacityBytes, + FreeBytes: probe.FreeBytes, FreeInodes: probe.FreeInodes, ToolVersions: probe.ToolVersions, + ObservedAt: probe.ObservedAt, ErrorCode: probe.ErrorCode, ErrorMessage: probe.ErrorMessage, + RepositoryUsages: usages, RepositoryUsageSnapshot: probe.RepositoryUsageSnapshot, + }) +} + +func (a *application) ListRepositoryPreflightRecords(ctx entity.Context, + req *repositorypb.ListRepositoryPreflightRecordsRequest, +) ([]po.RepositoryPreflightRecord, request.PageResponse, error) { + if req == nil { + return nil, request.PageResponse{}, fmt.Errorf("request is required") + } + return a.repositoryNodeSrv.ListPreflights(ctx, request.ListRepositoryPreflightRecordsRequest{ + StorageNodeID: req.GetStorageNodeId(), RepositoryID: req.GetRepositoryId(), + PageRequest: request.ToPageRequest(req.GetPageRequest()), + }) +} + +func (a *application) CreateRepositoryNodeEnrollment(ctx entity.Context, + req *repositorypb.CreateRepositoryNodeEnrollmentRequest, +) (*repositoryNodeService.EnrollmentResult, error) { + if req == nil || req.GetSpec() == nil { + return nil, fmt.Errorf("repository node enrollment spec is required") + } + spec := req.GetSpec() + labels := make(map[string]string, len(spec.GetLabels())) + for key, value := range spec.GetLabels() { + labels[strings.TrimSpace(key)] = strings.TrimSpace(value) + } + architecture := architectureFromPB(spec.GetExpectedArchitecture()) + if spec.GetTmsAgentId() == 0 { + return nil, fmt.Errorf("repository node installation requires a TMS agent") + } + if a.repositoryTMS == nil { + return nil, fmt.Errorf("repository node installation requires TMS") + } + agents, err := a.repositoryTMS.ListNodes(ctx) + if err != nil { + return nil, fmt.Errorf("list TMS agents for repository node installation: %w", err) + } + observedIP := "" + for _, agent := range agents { + if agent.AgentID != spec.GetTmsAgentId() { + continue + } + if !agent.Connected { + return nil, nodeinstaller.ErrTargetOffline + } + observedIP = strings.TrimSpace(agent.IP) + break + } + if observedIP == "" { + return nil, nodeinstaller.ErrTargetNotFound + } + selection, err := a.repositoryNodeSrv.SelectInstallArtifact(ctx, spec.GetProjectId(), architecture, + strings.TrimSpace(spec.GetArtifactId())) + if err != nil { + return nil, err + } + result, err := a.repositoryNodeSrv.CreateEnrollment(ctx, request.RepositoryNodeEnrollmentSpec{ + Node: request.RepositoryStorageNodeSpec{ProjectID: spec.GetProjectId(), Name: spec.GetName(), + Description: spec.GetDescription(), ControlBaseURL: spec.GetControlBaseUrl(), + PublishBaseURL: spec.GetPublishBaseUrl(), TLSServerName: spec.GetTlsServerName(), + Enabled: true, Labels: labels}, + TMSAgentID: spec.GetTmsAgentId(), ExpectedArchitecture: architecture, + ObservedIP: observedIP, ArtifactID: selection.Artifact.ID, + }) + if err != nil || spec.GetTmsAgentId() == 0 { + return result, err + } + if a.repositoryNodeInstaller == nil { + _ = a.repositoryNodeSrv.FailEnrollment(result.Enrollment.ID, "REPOSITORY_NODE_INSTALLER_UNAVAILABLE", + "repository node TMS installer is unavailable") + return nil, fmt.Errorf("repository node TMS installer is unavailable") + } + artifact := selection.Artifact + gpgKeyURLs := make([]string, 0, 2) + if request.RepositoryGPGPolicyEnforced(selection.Repository.GPGPolicy) { + keys, keyErr := a.repositoryContentSrv.ListGPGKeys(ctx, selection.Repository.ID) + if keyErr != nil || len(keys) == 0 { + if keyErr == nil { + keyErr = fmt.Errorf("Node RPM repository has no published GPG public key") + } + _ = a.repositoryNodeSrv.FailEnrollment(result.Enrollment.ID, + "REPOSITORY_NODE_ARTIFACT_GPG_KEY_UNAVAILABLE", keyErr.Error()) + return nil, keyErr + } + for _, key := range keys { + gpgKeyURLs = append(gpgKeyURLs, key.PublicKeyURL) + } + } + repoGPGCheck := selection.Revision.MetadataSigned && + selection.Revision.RepomdSignaturePath == "repodata/repomd.xml.asc" && + strings.TrimSpace(selection.Revision.MetadataPublicKeyURL) != "" + if repoGPGCheck { + gpgKeyURLs = append(gpgKeyURLs, selection.Revision.MetadataPublicKeyURL) + } + repositoryConfig := a.appConf.Application.Repository.WithDefaults() + bootstrapCA, err := os.ReadFile(repositoryConfig.NodeBootstrap.ServerCA) + if err != nil { + _ = a.repositoryNodeSrv.FailEnrollment(result.Enrollment.ID, "REPOSITORY_NODE_BOOTSTRAP_CA_UNAVAILABLE", err.Error()) + return nil, fmt.Errorf("read repository node bootstrap server CA: %w", err) + } + controlCA, err := os.ReadFile(repositoryConfig.InternalControl.ServerCA) + if err != nil { + _ = a.repositoryNodeSrv.FailEnrollment(result.Enrollment.ID, "REPOSITORY_NODE_CONTROL_CA_UNAVAILABLE", err.Error()) + return nil, fmt.Errorf("read repository node control server CA: %w", err) + } + dataClientCA, err := os.ReadFile(repositoryConfig.InternalControl.ClientCA) + if err != nil { + _ = a.repositoryNodeSrv.FailEnrollment(result.Enrollment.ID, "REPOSITORY_NODE_DATA_CLIENT_CA_UNAVAILABLE", err.Error()) + return nil, fmt.Errorf("read repository node data client CA: %w", err) + } + tmsTaskID, err := a.repositoryNodeInstaller.Dispatch(ctx, artifact, nodeinstaller.RepositorySource{ + BaseURL: selection.RevisionURL, PackageURL: artifact.ImmutableURL, + GPGCheck: request.RepositoryGPGPolicyEnforced(selection.Repository.GPGPolicy), + GPGKeyURL: strings.Join(gpgKeyURLs, " "), RepoGPGCheck: repoGPGCheck, + }, + nodeinstaller.BootstrapPayload{Token: result.BootstrapToken, NodeID: result.Node.ID, + EnrollmentID: result.Enrollment.ID, InstallTaskID: result.Task.ID, + TMSAgentID: result.Enrollment.TMSAgentID, + BootstrapTarget: repositoryConfig.NodeBootstrap.PublicTarget, + BootstrapServerName: repositoryConfig.NodeBootstrap.ServerName, BootstrapServerCA: bootstrapCA, + ControlTarget: repositoryConfig.InternalControl.PublicTarget, + ControlServerName: repositoryConfig.InternalControl.ServerName, ControlServerCA: controlCA, + DataClientCA: dataClientCA}, 900) + if err != nil { + _ = a.repositoryNodeSrv.FailEnrollment(result.Enrollment.ID, "REPOSITORY_NODE_TMS_INSTALL_FAILED", err.Error()) + return nil, err + } + if err = a.repositoryNodeSrv.MarkEnrollmentInstalling(result.Enrollment.ID, tmsTaskID); err != nil { + _ = a.repositoryNodeSrv.FailEnrollment(result.Enrollment.ID, "REPOSITORY_NODE_TMS_LINK_FAILED", err.Error()) + return nil, err + } + result.Enrollment.Status = request.RepositoryNodeEnrollmentInstalling + result.Enrollment.TMSTaskID = tmsTaskID + result.Node.Status = request.RepositoryStorageNodeStatusInstalling + result.Task.Status, result.Task.ProgressPercent = request.RepositoryTaskStatusRunning, 20 + result.Target.Status, result.Target.ProgressPercent = request.RepositoryTaskStatusRunning, 20 + return result, nil +} + +func (a *application) RegisterRepositoryNodeArtifact(ctx entity.Context, + req *repositorypb.RegisterRepositoryNodeArtifactRequest, +) (*po.RepositoryNodeArtifact, error) { + if req == nil || req.GetSpec() == nil { + return nil, fmt.Errorf("repository node artifact spec is required") + } + spec := req.GetSpec() + if strings.TrimSpace(spec.GetSourceRepositoryId()) != "" { + return a.repositoryNodeSrv.RegisterRepositoryArtifact(ctx, request.RepositoryNodeArtifactSpec{ + ProjectID: spec.GetProjectId(), SourceKind: request.RepositoryNodeArtifactRepository, + SourceRepositoryID: spec.GetSourceRepositoryId(), RevisionID: spec.GetRevisionId(), + PackageID: spec.GetPackageId(), Architecture: architectureFromPB(spec.GetArchitecture()), + BuildCommit: spec.GetBuildCommit(), Status: request.RepositoryNodeArtifactPublished, + }) + } + return a.repositoryNodeSrv.RegisterArtifact(ctx, request.RepositoryNodeArtifactSpec{ + Name: spec.GetName(), Version: spec.GetVersion(), Release: spec.GetRelease(), + Architecture: architectureFromPB(spec.GetArchitecture()), FileName: spec.GetFileName(), + SizeBytes: spec.GetSizeBytes(), SHA256: spec.GetSha256(), BuildCommit: spec.GetBuildCommit(), + RelativePath: spec.GetRelativePath(), Status: request.RepositoryNodeArtifactAvailable, + }) +} + +func (a *application) ConfigureRepositoryNodeInstallSource(ctx entity.Context, + req *repositorypb.ConfigureRepositoryNodeInstallSourceRequest, +) (*po.RepositoryNodeInstallSource, error) { + if req == nil || req.GetSpec() == nil { + return nil, fmt.Errorf("repository node install source spec is required") + } + spec := req.GetSpec() + return a.repositoryNodeSrv.ConfigureInstallSource(ctx, request.RepositoryNodeInstallSourceSpec{ + ProjectID: spec.GetProjectId(), RepositoryID: spec.GetRepositoryId(), + Architecture: architectureFromPB(spec.GetArchitecture()), Status: spec.GetStatus(), + }) +} + +func (a *application) ListRepositoryNodeInstallSources(ctx entity.Context, + req *repositorypb.ListRepositoryNodeInstallSourcesRequest, +) ([]po.RepositoryNodeInstallSource, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + return a.repositoryNodeSrv.ListInstallSources(ctx, req.GetProjectId()) +} + +func (a *application) UpsertRepositoryNodeRegistrationAllowlist(ctx entity.Context, + req *repositorypb.UpsertRepositoryNodeRegistrationAllowlistRequest, +) (*po.RepositoryNodeRegistrationAllowlist, error) { + if req == nil || req.GetSpec() == nil { + return nil, fmt.Errorf("repository node registration allowlist spec is required") + } + spec := req.GetSpec() + var expiresAt *time.Time + if value := spec.GetExpiresAt(); value != nil { + parsed := value.AsTime().UTC() + expiresAt = &parsed + } + return a.repositoryNodeSrv.UpsertRegistrationAllowlist(ctx, request.RepositoryNodeRegistrationAllowlistSpec{ + ProjectID: spec.GetProjectId(), TMSAgentID: spec.GetTmsAgentId(), + ExpectedHostname: spec.GetExpectedHostname(), ExpectedIP: spec.GetExpectedIp(), + ExpectedArchitecture: architectureFromPB(spec.GetExpectedArchitecture()), + Status: spec.GetStatus(), ExpiresAt: expiresAt, + }) +} + +func (a *application) ListRepositoryNodeRegistrationAllowlists(ctx entity.Context, + req *repositorypb.ListRepositoryNodeRegistrationAllowlistsRequest, +) ([]po.RepositoryNodeRegistrationAllowlist, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + return a.repositoryNodeSrv.ListRegistrationAllowlists(ctx, req.GetProjectId()) +} + +func (a *application) ListRepositoryNodeArtifacts(ctx entity.Context, + req *repositorypb.ListRepositoryNodeArtifactsRequest, +) ([]po.RepositoryNodeArtifact, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + return a.repositoryNodeSrv.ListArtifacts(ctx, req.GetProjectId(), req.GetStatus()) +} + +func (a *application) DownloadRepositoryNodeArtifact(ctx entity.Context, artifactID string) ( + *request.RepositoryNodeArtifactDownload, error, +) { + return a.repositoryNodeSrv.DownloadArtifact(ctx, artifactID) +} + +func repositoryStorageNodeSpecFromPB(spec *repositorypb.RepositoryStorageNodeSpec) ( + request.RepositoryStorageNodeSpec, error, +) { + if spec == nil { + return request.RepositoryStorageNodeSpec{}, fmt.Errorf("repository storage node spec is required") + } + enabled := true + if spec.Enabled != nil { + enabled = spec.GetEnabled() + } + labels := make(map[string]string, len(spec.GetLabels())) + for key, value := range spec.GetLabels() { + labels[strings.TrimSpace(key)] = strings.TrimSpace(value) + } + return request.RepositoryStorageNodeSpec{ + ProjectID: spec.GetProjectId(), Name: spec.GetName(), Description: spec.GetDescription(), + ControlBaseURL: spec.GetControlBaseUrl(), PublishBaseURL: spec.GetPublishBaseUrl(), + TLSServerName: spec.GetTlsServerName(), CertificateFingerprint: spec.GetCertificateFingerprint(), + Enabled: enabled, Labels: labels, + }, nil +} + +func storageNodeStatusFromPB(value repositorypb.RepositoryStorageNodeStatus) string { + switch value { + case repositorypb.RepositoryStorageNodeStatus_REPOSITORY_STORAGE_NODE_STATUS_UNKNOWN: + return request.RepositoryStorageNodeStatusUnknown + case repositorypb.RepositoryStorageNodeStatus_REPOSITORY_STORAGE_NODE_STATUS_ONLINE: + return request.RepositoryStorageNodeStatusOnline + case repositorypb.RepositoryStorageNodeStatus_REPOSITORY_STORAGE_NODE_STATUS_OFFLINE: + return request.RepositoryStorageNodeStatusOffline + case repositorypb.RepositoryStorageNodeStatus_REPOSITORY_STORAGE_NODE_STATUS_DISABLED: + return request.RepositoryStorageNodeStatusDisabled + case repositorypb.RepositoryStorageNodeStatus_REPOSITORY_STORAGE_NODE_STATUS_PENDING: + return request.RepositoryStorageNodeStatusPending + case repositorypb.RepositoryStorageNodeStatus_REPOSITORY_STORAGE_NODE_STATUS_INSTALLING: + return request.RepositoryStorageNodeStatusInstalling + case repositorypb.RepositoryStorageNodeStatus_REPOSITORY_STORAGE_NODE_STATUS_INSTALL_FAILED: + return request.RepositoryStorageNodeStatusInstallFailed + default: + return "" + } +} diff --git a/manager/backend/services/footstone/application/repository_node_failure_integration_test.go b/manager/backend/services/footstone/application/repository_node_failure_integration_test.go new file mode 100644 index 0000000000000000000000000000000000000000..4dfd77176d39e548058869403e70bbb1300d611d --- /dev/null +++ b/manager/backend/services/footstone/application/repository_node_failure_integration_test.go @@ -0,0 +1,549 @@ +//go:build integration + +package application + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/hex" + "encoding/json" + "encoding/pem" + "io" + "math/big" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + baseconfig "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + ftpb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/footstone" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorycontentrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositoryrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorystoragenoderepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryContentService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryNodeService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryTaskService" + appconfig "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/nodeclient" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/nodeprotocol" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/tasklog" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/taskresult" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/repositoryworker/nodeapi" +) + +const ( + applicationFailureProjectID int64 = 920004 + applicationFailureRepoID = "77777777-7777-4777-8777-777777777771" + applicationFailureNodeID = "88888888-8888-4888-8888-888888888881" + applicationFailureUploadID = "99999999-9999-4999-8999-999999999991" + applicationFailureActiveRevision = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaab" +) + +func TestReportRepositoryNodeTaskFailureOverMTLSWithMySQL(t *testing.T) { + dsn := os.Getenv("OCM_REPOSITORY_TEST_DSN") + if dsn == "" { + t.Skip("OCM_REPOSITORY_TEST_DSN is not set") + } + db, err := database.NewDBClient(baseconfig.AccessPoint{Source: dsn}) + if err != nil { + t.Fatalf("connect test MySQL: %v", err) + } + defer db.Close() + if err = db.DB().AutoMigrate(&po.Project{}); err != nil { + t.Fatalf("create disposable project table: %v", err) + } + repositoryService := repositoryService.New(db) + taskService := repositoryTaskService.New(db) + nodeService := repositoryNodeService.New(db) + if err = repositoryService.SyncTables(); err != nil { + t.Fatal(err) + } + if err = taskService.SyncTables(); err != nil { + t.Fatal(err) + } + if err = nodeService.SyncTables(); err != nil { + t.Fatal(err) + } + store, err := storage.New(appconfig.RepositoryConfig{Root: filepath.Join(t.TempDir(), "repository-data"), + MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + contentService := repositoryContentService.New(db, repositoryService, taskService, store) + if err = contentService.SyncTables(); err != nil { + t.Fatal(err) + } + if err = db.DB().AutoMigrate(&po.RepositoryTaskResultItem{}); err != nil { + t.Fatalf("create disposable task result table: %v", err) + } + cleanupApplicationFailureRows(db) + defer cleanupApplicationFailureRows(db) + + project := &po.Project{Base: po.Base{ID: applicationFailureProjectID}, Name: "repository-app-mtls", + EnName: "repository-app-mtls", Creator: "repository-admin"} + if err = db.DB().Create(project).Error; err != nil { + t.Fatal(err) + } + + pkiRoot := t.TempDir() + ca, caKey, caPath := createApplicationTestCA(t, pkiRoot) + serverCert, serverKey := issueApplicationTestCertificate(t, pkiRoot, "node-server", ca, caKey, false) + clientCert, clientKey := issueApplicationTestCertificate(t, pkiRoot, "control-client", ca, caKey, true) + nodeServer, err := nodeapi.New(appconfig.RepositoryNodeServerConfig{MaxResultBytes: 1 << 20}, store) + if err != nil { + t.Fatal(err) + } + httpServer := httptest.NewUnstartedServer(nodeServer.Handler()) + serverCertificate, err := tls.LoadX509KeyPair(serverCert, serverKey) + if err != nil { + t.Fatal(err) + } + clientCAs := x509.NewCertPool() + clientCAs.AddCert(ca) + httpServer.TLS = &tls.Config{Certificates: []tls.Certificate{serverCertificate}, ClientCAs: clientCAs, + ClientAuth: tls.RequireAndVerifyClientCert, MinVersion: tls.VersionTLS12} + httpServer.StartTLS() + defer httpServer.Close() + + nodeRepository := repositorystoragenoderepo.New(db) + now := time.Now().UTC() + node := &po.RepositoryStorageNode{ID: applicationFailureNodeID, ProjectID: applicationFailureProjectID, + Name: "repository-app-node", ControlBaseURL: httpServer.URL, + PublishBaseURL: "http://repository-node.invalid:18090", TLSServerName: "repository-node.test", + Status: request.RepositoryStorageNodeStatusOnline, Enabled: true, FilesystemType: "xfs", + CapacityBytes: 1 << 30, FreeBytes: 1 << 29, FreeInodes: 100000, + ToolVersionsJSON: []byte(`{}`), LabelsJSON: []byte(`{}`), LastHeartbeatAt: &now, + CertificateFingerprint: strings.Repeat("a", 64), CreatedBy: "repository-admin"} + if err = nodeRepository.CreateNode(node, nil); err != nil { + t.Fatal(err) + } + repository := &po.SoftwareRepository{ID: applicationFailureRepoID, ProjectID: applicationFailureProjectID, + SystemProduct: "OpenCloudOS", ProductVersion: "9.6", Name: "application-mtls-private", + Architecture: request.RepositoryArchitectureX8664, Type: request.RepositoryTypePrivate, + StorageLocation: "public/application-mtls-private", PublishKey: "application-mtls-private", + AllowUpload: true, IncludePackagesJSON: "[]", ExcludePackagesJSON: "[]", + GPGPolicy: request.RepositoryGPGDisabled, Enabled: true, Status: request.RepositoryStatusReady, + ActiveRevisionID: applicationFailureActiveRevision, PrimaryStorageNodeID: applicationFailureNodeID, + PublishBaseURLSnapshot: node.PublishBaseURL, Creator: "repository-admin"} + if err = repositoryrepo.New(db).Create(repository, nil); err != nil { + t.Fatal(err) + } + + uploadRelative, err := nodeprotocol.UploadRelativePath(applicationFailureUploadID) + if err != nil { + t.Fatal(err) + } + snapshot := request.RepositoryUploadSnapshot{UploadID: applicationFailureUploadID, + FileName: "damaged.rpm", Kind: request.RepositoryUploadKindRPM, RelativePath: uploadRelative, + SizeBytes: 7, SHA256: strings.Repeat("a", 64)} + payload, err := json.Marshal(snapshot) + if err != nil { + t.Fatal(err) + } + ctx := entity.NewDefaultContext() + ctx.User = &ftpb.UserInfo{LoginName: "repository-admin"} + task, err := taskService.Enqueue(ctx, request.EnqueueRepositoryTaskRequest{RepositoryID: repository.ID, + Type: request.RepositoryTaskTypeUpload, PayloadJSON: string(payload), MaxAttempts: 2}) + if err != nil { + t.Fatal(err) + } + upload := &po.RepositoryUpload{ID: snapshot.UploadID, RepositoryID: repository.ID, + ProjectID: applicationFailureProjectID, StorageNodeID: node.ID, TaskID: task.ID, + FileName: snapshot.FileName, Kind: snapshot.Kind, ExpectedSize: snapshot.SizeBytes, + ReceivedSize: snapshot.SizeBytes, ActualSHA256: snapshot.SHA256, RelativePath: snapshot.RelativePath, + Status: request.RepositoryUploadStatusReady, RequestedBy: "repository-admin", ExpiresAt: now.Add(time.Hour)} + contentRepository := repositorycontentrepo.New(db) + if err = contentRepository.CreateUpload(upload, nil); err != nil { + t.Fatal(err) + } + claimed, err := taskService.ClaimNextForNode(node.ID, "application-mtls-worker", 30*time.Second) + if err != nil || claimed == nil || claimed.ID != task.ID { + t.Fatalf("claim task/result = %#v/%v", claimed, err) + } + quarantine := []po.RepositoryQuarantineItem{{RepositoryID: repository.ID, TaskID: task.ID, + UploadID: upload.ID, OriginalName: upload.FileName, + RelativePath: filepath.ToSlash(filepath.Join("quarantine", repository.ID, task.ID, upload.FileName)), + SizeBytes: upload.ExpectedSize, SHA256: upload.ActualSHA256, + ReasonCode: request.RepositoryQuarantineDamagedRPM, + ReasonMessage: "rpm integrity verification failed"}} + objectID, err := nodeprotocol.TaskResultObjectID(task.ID) + if err != nil { + t.Fatal(err) + } + objectPath, err := store.Resolve(objectID) + if err != nil { + t.Fatal(err) + } + if err = os.MkdirAll(filepath.Dir(objectPath), 0700); err != nil { + t.Fatal(err) + } + resultFile, err := os.OpenFile(objectPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) + if err != nil { + t.Fatal(err) + } + const errorCode = "ALL_PACKAGES_REJECTED" + const errorMessage = "all packages were rejected" + if err = taskresult.WriteFailureZstd(resultFile, claimed, upload, quarantine, errorCode, errorMessage); err != nil { + _ = resultFile.Close() + t.Fatal(err) + } + if err = resultFile.Close(); err != nil { + t.Fatal(err) + } + resultBytes, err := os.ReadFile(objectPath) + if err != nil { + t.Fatal(err) + } + resultDigest := sha256.Sum256(resultBytes) + resultSHA := hex.EncodeToString(resultDigest[:]) + logWriter, err := tasklog.New(store, task.ID) + if err != nil { + t.Fatal(err) + } + if err = logWriter.Event("ERROR", "rpm-validation", errorMessage, map[string]string{"error_code": errorCode}); err != nil { + t.Fatal(err) + } + if _, err = logWriter.Close(request.RepositoryTaskStatusFailed, errorCode, errorMessage); err != nil { + t.Fatal(err) + } + resultClient, err := nodeclient.New(appconfig.RepositoryNodeDataClientConfig{Enabled: true, + ClientCert: clientCert, ClientKey: clientKey, ServerCA: caPath, TimeoutSeconds: 5, + MaxResultBytes: 1 << 20}, store.Root()) + if err != nil { + t.Fatal(err) + } + app := &application{repositorySrv: repositoryService, repositoryContentSrv: contentService, + repositoryNodeSrv: nodeService, repositoryTaskSrv: taskService, repositoryNodeResult: resultClient} + input := request.CompleteRepositoryTaskRequest{TaskID: task.ID, StorageNodeID: node.ID, + WorkerID: claimed.WorkerID, FencingToken: claimed.FencingToken, + Status: request.RepositoryTaskStatusFailed, ErrorCode: errorCode, ErrorMessage: errorMessage, + ResultObjectID: objectID, ResultObjectSHA256: strings.Repeat("f", 64), QuarantineCount: 1} + if _, err = app.ReportRepositoryNodeTaskFailure(context.Background(), input); err == nil { + t.Fatal("failure report accepted a mismatched result digest") + } + unchangedTask, _ := taskService.DescribeInternal(task.ID) + unchangedUpload, _ := contentRepository.GetUpload(upload.ID, false, nil) + unchangedQuarantine, _ := contentRepository.QuarantineByTask(task.ID, nil) + if unchangedTask.Status != request.RepositoryTaskStatusRunning || + unchangedUpload.Status != request.RepositoryUploadStatusReady || len(unchangedQuarantine) != 0 { + t.Fatalf("digest rejection changed database state: task=%s upload=%s quarantine=%d", + unchangedTask.Status, unchangedUpload.Status, len(unchangedQuarantine)) + } + + input.ResultObjectSHA256 = resultSHA + completed, err := app.ReportRepositoryNodeTaskFailure(context.Background(), input) + if err != nil || completed.Status != request.RepositoryTaskStatusFailed { + t.Fatalf("ReportRepositoryNodeTaskFailure() task/error = %#v/%v", completed, err) + } + completedAgain, err := app.ReportRepositoryNodeTaskFailure(context.Background(), input) + if err != nil || completedAgain.Status != request.RepositoryTaskStatusFailed { + t.Fatalf("idempotent failure report task/error = %#v/%v", completedAgain, err) + } + storedUpload, uploadErr := contentRepository.GetUpload(upload.ID, false, nil) + storedQuarantine, quarantineErr := contentRepository.QuarantineByTask(task.ID, nil) + activeRepository, repositoryErr := repositoryrepo.New(db).GetByID(repository.ID, nil) + if uploadErr != nil || quarantineErr != nil || repositoryErr != nil || + storedUpload.Status != request.RepositoryUploadStatusFailed || len(storedQuarantine) != 1 || + activeRepository.ActiveRevisionID != applicationFailureActiveRevision { + t.Fatalf("mTLS failure import state = upload:%#v quarantine:%d repository:%#v errors:%v/%v/%v", + storedUpload, len(storedQuarantine), activeRepository, uploadErr, quarantineErr, repositoryErr) + } + logIndex, err := app.DescribeRepositoryTaskLog(ctx, task.ID) + if err != nil || logIndex.TaskID != task.ID || logIndex.StorageNodeID != node.ID || + logIndex.CompressedBytes <= 0 || logIndex.UncompressedBytes <= 0 || logIndex.Summary == "" { + t.Fatalf("DescribeRepositoryTaskLog() index/error = %#v/%v", logIndex, err) + } + var storedLogShape struct { + SummaryBytes int64 + PathBytes int64 + CompressedBytes int64 + UncompressedBytes int64 + } + if err = db.DB().Raw(`SELECT OCTET_LENGTH(summary) AS summary_bytes, + OCTET_LENGTH(relative_path) AS path_bytes, compressed_bytes, uncompressed_bytes + FROM repository_task_log_indexes WHERE task_id = ?`, task.ID).Scan(&storedLogShape).Error; err != nil { + t.Fatal(err) + } + if storedLogShape.SummaryBytes <= 0 || storedLogShape.SummaryBytes > 64<<10 || + storedLogShape.PathBytes <= 0 || storedLogShape.PathBytes > 512 || + storedLogShape.CompressedBytes != logIndex.CompressedBytes || + storedLogShape.UncompressedBytes != logIndex.UncompressedBytes { + t.Fatalf("task log database shape = %#v", storedLogShape) + } + t.Logf("task log database index only: summary_bytes=%d path_bytes=%d compressed=%d uncompressed=%d", + storedLogShape.SummaryBytes, storedLogShape.PathBytes, + storedLogShape.CompressedBytes, storedLogShape.UncompressedBytes) + download, err := app.DownloadRepositoryTaskLog(ctx, task.ID) + if err != nil { + t.Fatal(err) + } + downloaded, readErr := io.ReadAll(download.Content) + closeErr := download.Content.Close() + downloadDigest := sha256.Sum256(downloaded) + if readErr != nil || closeErr != nil || int64(len(downloaded)) != download.SizeBytes || + hex.EncodeToString(downloadDigest[:]) != download.SHA256 || download.SHA256 == logIndex.SHA256 || + download.ContentType != "application/gzip" || !strings.HasSuffix(download.FileName, "-logs.tar.gz") { + t.Fatalf("DownloadRepositoryTaskLog() size/digest/errors = %d/%s/%v/%v", + len(downloaded), download.SHA256, readErr, closeErr) + } + gzipReader, err := gzip.NewReader(bytes.NewReader(downloaded)) + if err != nil { + t.Fatal(err) + } + archive := tar.NewReader(gzipReader) + bundleEntries := map[string]bool{} + for { + header, archiveErr := archive.Next() + if archiveErr == io.EOF { + break + } + if archiveErr != nil { + t.Fatal(archiveErr) + } + bundleEntries[header.Name] = true + } + for _, name := range []string{"context.json", "result-summary.json", "task-log.jsonl.zst", "SHA256SUMS"} { + if !bundleEntries[name] { + t.Fatalf("task log bundle entries = %#v, missing %s", bundleEntries, name) + } + } + t.Logf("mTLS failure import: task=%s upload=%s quarantine=%d active_revision=%s result_sha256=%s", + completed.Status, storedUpload.Status, len(storedQuarantine), activeRepository.ActiveRevisionID, resultSHA) + + t.Run("authorized upload retry imports its own failure idempotently", func(t *testing.T) { + retry, retryErr := taskService.Retry(ctx, task.ID) + if retryErr != nil { + t.Fatal(retryErr) + } + retryClaim, retryErr := taskService.ClaimNextForNode(node.ID, "application-mtls-retry-worker", 30*time.Second) + if retryErr != nil || retryClaim == nil || retryClaim.ID != retry.ID { + t.Fatalf("claim retry task = %#v/%v", retryClaim, retryErr) + } + retryQuarantine := quarantine[0] + retryQuarantine.TaskID = retry.ID + retryQuarantine.RelativePath = filepath.ToSlash(filepath.Join("quarantine", repository.ID, retry.ID, upload.FileName)) + retryObjectID, retryErr := nodeprotocol.TaskResultObjectID(retry.ID) + if retryErr != nil { + t.Fatal(retryErr) + } + retryObjectPath, retryErr := store.Resolve(retryObjectID) + if retryErr != nil { + t.Fatal(retryErr) + } + if retryErr = os.MkdirAll(filepath.Dir(retryObjectPath), 0700); retryErr != nil { + t.Fatal(retryErr) + } + retryObject, retryErr := os.OpenFile(retryObjectPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) + if retryErr != nil { + t.Fatal(retryErr) + } + const retryMessage = "retry validation rejected the same frozen upload" + if retryErr = taskresult.WriteFailureZstd(retryObject, retryClaim, upload, + []po.RepositoryQuarantineItem{retryQuarantine}, errorCode, retryMessage); retryErr != nil { + _ = retryObject.Close() + t.Fatal(retryErr) + } + if retryErr = retryObject.Close(); retryErr != nil { + t.Fatal(retryErr) + } + retryBytes, retryErr := os.ReadFile(retryObjectPath) + if retryErr != nil { + t.Fatal(retryErr) + } + retryDigest := sha256.Sum256(retryBytes) + retryLog, retryErr := tasklog.New(store, retry.ID) + if retryErr != nil { + t.Fatal(retryErr) + } + if retryErr = retryLog.Event("ERROR", "rpm-validation", retryMessage, nil); retryErr != nil { + t.Fatal(retryErr) + } + if _, retryErr = retryLog.Close(request.RepositoryTaskStatusFailed, errorCode, retryMessage); retryErr != nil { + t.Fatal(retryErr) + } + retryInput := request.CompleteRepositoryTaskRequest{ + TaskID: retry.ID, StorageNodeID: node.ID, WorkerID: retryClaim.WorkerID, FencingToken: retryClaim.FencingToken, + Status: request.RepositoryTaskStatusFailed, ErrorCode: errorCode, ErrorMessage: retryMessage, + ResultObjectID: retryObjectID, ResultObjectSHA256: hex.EncodeToString(retryDigest[:]), QuarantineCount: 1, + } + for iteration := 0; iteration < 2; iteration++ { + result, reportErr := app.ReportRepositoryNodeTaskFailure(context.Background(), retryInput) + if reportErr != nil || result == nil || result.Status != request.RepositoryTaskStatusFailed { + t.Fatalf("retry failure report %d = %#v/%v", iteration, result, reportErr) + } + } + afterUpload, uploadErr := contentRepository.GetUpload(upload.ID, false, nil) + afterQuarantine, quarantineErr := contentRepository.QuarantineByTask(retry.ID, nil) + if uploadErr != nil || quarantineErr != nil || afterUpload.Status != request.RepositoryUploadStatusFailed || + afterUpload.ErrorMessage != retryMessage || len(afterQuarantine) != 1 { + t.Fatalf("retry failure import = upload:%#v quarantine:%d error:%v/%v", + afterUpload, len(afterQuarantine), uploadErr, quarantineErr) + } + }) + t.Run("authorized upload retry succeeds atomically and prepare replay is idempotent", func(t *testing.T) { + retry, retryErr := taskService.Retry(ctx, task.ID) + if retryErr != nil { + t.Fatal(retryErr) + } + claim, retryErr := taskService.ClaimNextForNode(node.ID, "application-upload-success", 30*time.Second) + if retryErr != nil || claim == nil || claim.ID != retry.ID { + t.Fatalf("claim: %#v/%v", claim, retryErr) + } + // Even a valid task identity must not bypass the explicit retry preparation. + tx := db.DB().Begin() + if retryErr = contentService.ApplyRemoteUploadOutcome(claim, snapshot, request.RepositoryTaskStatusSuccess, "", "", tx); retryErr == nil { + _ = tx.Rollback() + t.Fatal("ordinary FAILED upload imported without preparation") + } + _ = tx.Rollback() + const revisionID = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb1" + publishedAt := time.Now().UTC().Truncate(time.Millisecond) + revision := &po.RepositoryRevision{ID: revisionID, RepositoryID: repository.ID, StorageNodeID: node.ID, TaskID: claim.ID, + Kind: request.RepositoryRevisionKindUpload, Status: request.RepositoryRevisionStatusPublished, PackageCount: 1, TotalBytes: snapshot.SizeBytes, + ManifestSHA256: strings.Repeat("f", 64), RepomdSHA256: strings.Repeat("a", 64), SnapshotManifestSHA256: strings.Repeat("d", 64), + RelativePath: "public/application-mtls-private/revisions/" + revisionID, CreatedBy: claim.RequestedBy, PublishedAt: &publishedAt} + packages := []po.RepositoryPackage{{ID: "cccccccc-cccc-4ccc-8ccc-ccccccccccc1", RepositoryID: repository.ID, RevisionID: revisionID, + Name: "retry-fixture", Epoch: "0", Version: "1", Release: "1", Architecture: "noarch", NEVRA: "retry-fixture-0:1-1.noarch", + FileName: snapshot.FileName, RelativePath: "content/" + snapshot.FileName, SizeBytes: snapshot.SizeBytes, SHA256: snapshot.SHA256, + Source: "UPLOAD", SourceUploadID: snapshot.UploadID}} + input := request.PrepareRepositoryTaskCommitRequest{TaskID: claim.ID, WorkerID: claim.WorkerID, FencingToken: claim.FencingToken, + StorageNodeID: node.ID, RevisionID: revisionID, ManifestSHA256: revision.SnapshotManifestSHA256, + ResultObjectID: "task-results/" + claim.ID + "/result.jsonl.zst", ResultObjectSHA256: strings.Repeat("e", 64)} + mutate := func(session entity.Session, repeated bool) error { + if !repeated { + if prepErr := contentService.PrepareRemoteUploadRetry(claim, snapshot, session); prepErr != nil { + return prepErr + } + } + if importErr := contentService.ImportRemoteRevision(revision, packages, nil, session); importErr != nil { + return importErr + } + return contentService.ApplyRemoteUploadOutcome(claim, snapshot, request.RepositoryTaskStatusSuccess, "", "", session) + } + var prepared *po.RepositoryTask + for iteration := 0; iteration < 2; iteration++ { + prepared, retryErr = taskService.PrepareCommitWithMutation(input, mutate) + if retryErr != nil { + t.Fatalf("prepare/replay %d: %v", iteration, retryErr) + } + } + confirm := request.ConfirmRepositoryTaskCommitRequest{TaskID: claim.ID, StorageNodeID: node.ID, WorkerID: claim.WorkerID, + FencingToken: claim.FencingToken, CommitToken: prepared.CommitToken} + for iteration := 0; iteration < 2; iteration++ { + completed, confirmErr := taskService.ConfirmAndCompleteCommit(confirm) + if confirmErr != nil || completed.Status != request.RepositoryTaskStatusSuccess { + t.Fatalf("confirm/replay %d: %#v/%v", iteration, completed, confirmErr) + } + } + stored, readErr := contentRepository.GetUpload(upload.ID, false, nil) + storedPackages, packagesErr := contentRepository.PackagesByRevision(revisionID, nil) + active, activeErr := repositoryrepo.New(db).GetByID(repository.ID, nil) + if readErr != nil || packagesErr != nil || activeErr != nil || stored.Status != request.RepositoryUploadStatusCompleted || + stored.ErrorCode != "" || stored.ErrorMessage != "" || len(storedPackages) != 1 || active.ActiveRevisionID != revisionID { + t.Fatalf("atomic successful retry: upload=%#v packages=%d active=%#v errors=%v/%v/%v", stored, len(storedPackages), active, readErr, packagesErr, activeErr) + } + t.Log("real MySQL FAILED -> READY -> COMPLETED, one accepted package/revision, prepare/confirm replay PASS; result metadata fixture, not RPM verification") + }) +} + +func cleanupApplicationFailureRows(db *database.DBClient) { + db.DB().Where("task_id IN (SELECT id FROM repository_tasks WHERE repository_id = ?)", applicationFailureRepoID). + Delete(&po.RepositoryTaskLogIndex{}) + db.DB().Where("repository_id = ?", applicationFailureRepoID).Delete(&po.RepositoryQuarantineItem{}) + db.DB().Where("repository_id = ?", applicationFailureRepoID).Delete(&po.RepositoryUpload{}) + db.DB().Where("repository_id = ?", applicationFailureRepoID).Delete(&po.RepositoryPackage{}) + db.DB().Where("repository_id = ?", applicationFailureRepoID).Delete(&po.RepositoryRevision{}) + db.DB().Where("repository_id = ?", applicationFailureRepoID).Delete(&po.RepositoryTask{}) + db.DB().Where("repository_id = ?", applicationFailureRepoID).Delete(&po.RepositoryMutationLock{}) + db.DB().Where("id = ?", applicationFailureRepoID).Delete(&po.SoftwareRepository{}) + db.DB().Where("storage_node_id = ?", applicationFailureNodeID).Delete(&po.RepositoryPreflightRecord{}) + db.DB().Where("id = ?", applicationFailureNodeID).Delete(&po.RepositoryStorageNode{}) + db.DB().Where("id = ?", applicationFailureProjectID).Delete(&po.Project{}) +} + +func createApplicationTestCA(t *testing.T, root string) (*x509.Certificate, *rsa.PrivateKey, string) { + t.Helper() + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + template := &x509.Certificate{SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "repository-app-ca"}, + NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour), IsCA: true, + BasicConstraintsValid: true, KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature} + der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + if err != nil { + t.Fatal(err) + } + certificate, err := x509.ParseCertificate(der) + if err != nil { + t.Fatal(err) + } + path := filepath.Join(root, "ca.crt") + writeApplicationTestPEM(t, path, "CERTIFICATE", der) + return certificate, key, path +} + +func issueApplicationTestCertificate(t *testing.T, root, name string, ca *x509.Certificate, + caKey *rsa.PrivateKey, client bool, +) (string, string) { + t.Helper() + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + usage := []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth} + dnsNames := []string{"repository-node.test"} + if client { + usage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth} + dnsNames = nil + } + serialLimit := new(big.Int).Lsh(big.NewInt(1), 120) + serial, err := rand.Int(rand.Reader, serialLimit) + if err != nil { + t.Fatal(err) + } + template := &x509.Certificate{SerialNumber: serial, Subject: pkix.Name{CommonName: name}, + NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: usage, DNSNames: dnsNames} + der, err := x509.CreateCertificate(rand.Reader, template, ca, &key.PublicKey, caKey) + if err != nil { + t.Fatal(err) + } + certificatePath := filepath.Join(root, name+".crt") + keyPath := filepath.Join(root, name+".key") + writeApplicationTestPEM(t, certificatePath, "CERTIFICATE", der) + writeApplicationTestPEM(t, keyPath, "RSA PRIVATE KEY", x509.MarshalPKCS1PrivateKey(key)) + return certificatePath, keyPath +} + +func writeApplicationTestPEM(t *testing.T, path, blockType string, content []byte) { + t.Helper() + file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600) + if err != nil { + t.Fatal(err) + } + if err = pem.Encode(file, &pem.Block{Type: blockType, Bytes: content}); err != nil { + _ = file.Close() + t.Fatal(err) + } + if err = file.Close(); err != nil { + t.Fatal(err) + } +} diff --git a/manager/backend/services/footstone/application/repository_node_gpg_integration_test.go b/manager/backend/services/footstone/application/repository_node_gpg_integration_test.go new file mode 100644 index 0000000000000000000000000000000000000000..0cdddca59dc1375cb3030513876e6e16f2e16e47 --- /dev/null +++ b/manager/backend/services/footstone/application/repository_node_gpg_integration_test.go @@ -0,0 +1,242 @@ +//go:build integration + +package application + +import ( + "bytes" + "crypto/tls" + "crypto/x509" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/armor" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + baseconfig "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + ftpb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/footstone" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorycontentrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositoryrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorystoragenoderepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryContentService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryNodeService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryTaskService" + appconfig "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/nodeclient" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/repositoryworker/nodeapi" +) + +const ( + applicationGPGProjectID int64 = 920005 + applicationGPGRepoID = "77777777-7777-4777-8777-777777777772" + applicationGPGNodeID = "88888888-8888-4888-8888-888888888882" +) + +func TestRepositoryGPGKeyLifecycleOverMTLSWithMySQL(t *testing.T) { + dsn := os.Getenv("OCM_REPOSITORY_TEST_DSN") + if dsn == "" { + t.Skip("OCM_REPOSITORY_TEST_DSN is not set") + } + db, err := database.NewDBClient(baseconfig.AccessPoint{Source: dsn}) + if err != nil { + t.Fatalf("connect test MySQL: %v", err) + } + defer db.Close() + if err = db.DB().AutoMigrate(&po.Project{}); err != nil { + t.Fatal(err) + } + repositories := repositoryService.New(db) + tasks := repositoryTaskService.New(db) + nodes := repositoryNodeService.New(db) + for _, migrate := range []func() error{repositories.SyncTables, tasks.SyncTables, nodes.SyncTables} { + if err = migrate(); err != nil { + t.Fatal(err) + } + } + cleanupApplicationGPGRows(db) + defer cleanupApplicationGPGRows(db) + + project := &po.Project{Base: po.Base{ID: applicationGPGProjectID}, Name: "repository-gpg-mtls", + EnName: "repository-gpg-mtls", Creator: "repository-admin"} + if err = db.DB().Create(project).Error; err != nil { + t.Fatal(err) + } + + controlStore, err := storage.New(appconfig.RepositoryConfig{ + Root: filepath.Join(t.TempDir(), "control-plane-data"), MinimumFreeBytes: 1, + }) + if err != nil { + t.Fatal(err) + } + nodeStore, err := storage.New(appconfig.RepositoryConfig{ + Root: filepath.Join(t.TempDir(), "repository-node-data"), MinimumFreeBytes: 1, + }) + if err != nil { + t.Fatal(err) + } + + pkiRoot := t.TempDir() + ca, caKey, caPath := createApplicationTestCA(t, pkiRoot) + serverCert, serverKey := issueApplicationTestCertificate(t, pkiRoot, "node-server", ca, caKey, false) + clientCert, clientKey := issueApplicationTestCertificate(t, pkiRoot, "control-client", ca, caKey, true) + nodeServer, err := nodeapi.New(appconfig.RepositoryNodeServerConfig{MaxResultBytes: 1 << 20}, nodeStore) + if err != nil { + t.Fatal(err) + } + httpServer := httptest.NewUnstartedServer(nodeServer.Handler()) + serverCertificate, err := tls.LoadX509KeyPair(serverCert, serverKey) + if err != nil { + t.Fatal(err) + } + clientCAs := x509.NewCertPool() + clientCAs.AddCert(ca) + httpServer.TLS = &tls.Config{Certificates: []tls.Certificate{serverCertificate}, ClientCAs: clientCAs, + ClientAuth: tls.RequireAndVerifyClientCert, MinVersion: tls.VersionTLS12} + httpServer.StartTLS() + defer httpServer.Close() + + now := time.Now().UTC() + node := &po.RepositoryStorageNode{ID: applicationGPGNodeID, ProjectID: applicationGPGProjectID, + Name: "repository-gpg-node", ControlBaseURL: httpServer.URL, + PublishBaseURL: "https://repository-node.test:18090/repositories", + TLSServerName: "repository-node.test", Status: request.RepositoryStorageNodeStatusOnline, + Enabled: true, FilesystemType: "xfs", CapacityBytes: 1 << 30, FreeBytes: 1 << 29, + FreeInodes: 100000, ToolVersionsJSON: []byte(`{}`), LabelsJSON: []byte(`{}`), + LastHeartbeatAt: &now, CertificateFingerprint: strings.Repeat("b", 64), CreatedBy: "repository-admin"} + if err = repositorystoragenoderepo.New(db).CreateNode(node, nil); err != nil { + t.Fatal(err) + } + repository := &po.SoftwareRepository{ID: applicationGPGRepoID, ProjectID: applicationGPGProjectID, + SystemProduct: "OpenCloudOS", ProductVersion: "9.6", Name: "application-gpg-private", + Architecture: request.RepositoryArchitectureX8664, Type: request.RepositoryTypePrivate, + StorageLocation: "public/application-gpg-private", PublishKey: "application-gpg-private", + AllowUpload: true, IncludePackagesJSON: "[]", ExcludePackagesJSON: "[]", + GPGPolicy: request.RepositoryGPGVerifyPackage, Enabled: true, Status: request.RepositoryStatusEmpty, + PrimaryStorageNodeID: node.ID, PublishBaseURLSnapshot: node.PublishBaseURL, Creator: "repository-admin"} + if err = repositoryrepo.New(db).Create(repository, nil); err != nil { + t.Fatal(err) + } + + dataClient, err := nodeclient.New(appconfig.RepositoryNodeDataClientConfig{Enabled: true, + ClientCert: clientCert, ClientKey: clientKey, ServerCA: caPath, TimeoutSeconds: 5, + MaxResultBytes: 1 << 20}, controlStore.Root()) + if err != nil { + t.Fatal(err) + } + content := repositoryContentService.New(db, repositories, tasks, controlStore) + if err = content.SyncTables(); err != nil { + t.Fatal(err) + } + content.ConfigureRemoteUploads(nodes, &repositoryNodeUploadAdapter{client: dataClient}) + ctx := entity.NewDefaultContext() + ctx.User = &ftpb.UserInfo{LoginName: "repository-admin", + Roles: []*ftpb.ModuleRole{{RoleId: request.DefaultGlobalRoleName}}} + + registered, err := content.RegisterGPGKey(ctx, repository.ID, applicationTestPublicKey(t)) + if err != nil { + t.Fatal(err) + } + if registered.KeyID == "" || registered.Fingerprint == "" || registered.PublicKeyPath == "" || + !strings.Contains(registered.PublicKeyURL, "/"+repository.PublishKey+"/keys/") { + t.Fatalf("registered remote GPG key = %#v", registered) + } + privatePath, err := nodeStore.Resolve(registered.PublicKeyPath) + if err != nil { + t.Fatal(err) + } + publicRelative, err := nodeStore.PublicGPGKeyRelativePath(repository.PublishKey, registered.Fingerprint) + if err != nil { + t.Fatal(err) + } + publicPath, err := nodeStore.Resolve(publicRelative) + if err != nil { + t.Fatal(err) + } + for _, path := range []string{privatePath, publicPath} { + contentBytes, readErr := os.ReadFile(path) + if readErr != nil || !bytes.Contains(contentBytes, []byte("BEGIN PGP PUBLIC KEY BLOCK")) || + bytes.Contains(contentBytes, []byte("PRIVATE KEY")) { + t.Fatalf("repository node public key %s is invalid: %v", path, readErr) + } + } + controlRelative, err := controlStore.GPGKeyRelativePath(repository.ID, registered.Fingerprint) + if err != nil { + t.Fatal(err) + } + controlPath, err := controlStore.Resolve(controlRelative) + if err != nil { + t.Fatal(err) + } + if _, statErr := os.Lstat(controlPath); !os.IsNotExist(statErr) { + t.Fatalf("control plane unexpectedly stored repository key: %v", statErr) + } + + listed, err := content.ListGPGKeys(ctx, repository.ID) + if err != nil || len(listed) != 1 || listed[0].Fingerprint != registered.Fingerprint { + t.Fatalf("remote GPG key list = %#v/%v", listed, err) + } + metadata, err := content.ListGPGKeyMetadataInternal(repository.ID) + if err != nil || len(metadata) != 1 || metadata[0].PublicKeyPath != registered.PublicKeyPath { + t.Fatalf("task GPG key metadata = %#v/%v", metadata, err) + } + stored, err := repositorycontentrepo.New(db).GetGPGKey(repository.ID, registered.ID, nil) + if err != nil || stored.KeyID != registered.KeyID || stored.Fingerprint != registered.Fingerprint || + stored.PublicKeyPath != registered.PublicKeyPath { + t.Fatalf("database GPG metadata = %#v/%v", stored, err) + } + + if err = content.DeleteGPGKey(ctx, repository.ID, registered.ID, true); err != nil { + t.Fatal(err) + } + for _, path := range []string{privatePath, publicPath} { + if _, statErr := os.Lstat(path); !os.IsNotExist(statErr) { + t.Fatalf("deleted GPG key remains on repository node: %s: %v", path, statErr) + } + } + if listed, err = content.ListGPGKeys(ctx, repository.ID); err != nil || len(listed) != 0 { + t.Fatalf("GPG keys after deletion = %#v/%v", listed, err) + } + t.Logf("remote_gpg_lifecycle=PASS repository=%s node=%s key_id=%s fingerprint=%s db_metadata_only=true", + repository.ID, node.ID, registered.KeyID, registered.Fingerprint) +} + +func applicationTestPublicKey(t *testing.T) []byte { + t.Helper() + value, err := openpgp.NewEntity("OCManager Repository Integration", "", "repository@example.test", nil) + if err != nil { + t.Fatal(err) + } + var output bytes.Buffer + writer, err := armor.Encode(&output, openpgp.PublicKeyType, nil) + if err != nil { + t.Fatal(err) + } + if err = value.Serialize(writer); err == nil { + err = writer.Close() + } else { + _ = writer.Close() + } + if err != nil { + t.Fatal(err) + } + return output.Bytes() +} + +func cleanupApplicationGPGRows(db *database.DBClient) { + db.DB().Where("repository_id = ?", applicationGPGRepoID).Delete(&po.RepositoryGPGKey{}) + db.DB().Where("repository_id = ?", applicationGPGRepoID).Delete(&po.RepositoryTask{}) + db.DB().Where("repository_id = ?", applicationGPGRepoID).Delete(&po.RepositoryMutationLock{}) + db.DB().Where("id = ?", applicationGPGRepoID).Delete(&po.SoftwareRepository{}) + db.DB().Where("storage_node_id = ?", applicationGPGNodeID).Delete(&po.RepositoryPreflightRecord{}) + db.DB().Where("id = ?", applicationGPGNodeID).Delete(&po.RepositoryStorageNode{}) + db.DB().Where("id = ?", applicationGPGProjectID).Delete(&po.Project{}) +} diff --git a/manager/backend/services/footstone/application/repository_node_install.go b/manager/backend/services/footstone/application/repository_node_install.go new file mode 100644 index 0000000000000000000000000000000000000000..ddf8a1a53733b2ab8027d94ecc7d7788cdb5042b --- /dev/null +++ b/manager/backend/services/footstone/application/repository_node_install.go @@ -0,0 +1,78 @@ +package application + +import ( + "context" + "fmt" + "strings" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/tmsclient" +) + +func (a *application) reconcileRepositoryNodeInstallations(ctx context.Context) error { + if a == nil || a.repositoryNodeSrv == nil || a.repositoryTMS == nil { + return nil + } + values, err := a.repositoryNodeSrv.ListActiveInstallations(100) + if err != nil { + return err + } + var firstError error + for index := range values { + value := &values[index] + if !time.Now().UTC().Before(value.ExpiresAt) { + if failErr := a.repositoryNodeSrv.ExpireEnrollment(value.ID, "REPOSITORY_NODE_BOOTSTRAP_TIMEOUT", + "repository node did not complete bootstrap before the enrollment expired"); failErr != nil && firstError == nil { + firstError = failErr + } + continue + } + // Enrollment creation and TMS dispatch are separate steps. A pending + // enrollment without a TMS task remains eligible for bootstrap until its + // deadline, but must not issue an empty TMS detail request. + if strings.TrimSpace(value.TMSTaskID) == "" { + continue + } + detail, detailErr := a.repositoryTMS.Detail(ctx, value.TMSTaskID) + if detailErr != nil { + if firstError == nil { + firstError = fmt.Errorf("query TMS task %s: %w", value.TMSTaskID, detailErr) + } + continue + } + status := strings.ToUpper(strings.TrimSpace(detail.Status)) + if status != request.RepositoryTaskStatusFailed && status != request.RepositoryTaskStatusCanceled && + status != "TIMEOUT" { + continue + } + // A sent ACK timeout means that TMS cannot confirm delivery. The agent may + // still receive and execute the artifact, so keep the one-time token valid + // until bootstrap succeeds or the enrollment itself expires. + if repositoryNodeInstallResultUncertain(detail, value.TMSAgentID) { + continue + } + message := "TMS repository node installation failed" + for _, target := range detail.Targets { + if target.AgentID == value.TMSAgentID && strings.TrimSpace(target.ErrorMessage) != "" { + message = target.ErrorMessage + break + } + } + if failErr := a.repositoryNodeSrv.FailEnrollment(value.ID, "REPOSITORY_NODE_TMS_INSTALL_FAILED", message); failErr != nil && firstError == nil { + firstError = failErr + } + } + return firstError +} + +func repositoryNodeInstallResultUncertain(detail tmsclient.TaskDetail, agentID uint64) bool { + for _, target := range detail.Targets { + if target.AgentID != agentID { + continue + } + value := strings.ToLower(strings.TrimSpace(target.ErrorCode + " " + target.ErrorMessage)) + return strings.Contains(value, "sent_ack_timeout") || strings.Contains(value, "sent ack timeout") + } + return false +} diff --git a/manager/backend/services/footstone/application/repository_node_install_test.go b/manager/backend/services/footstone/application/repository_node_install_test.go new file mode 100644 index 0000000000000000000000000000000000000000..8288e4d47fde7077864883dfd17f636b8d5f5641 --- /dev/null +++ b/manager/backend/services/footstone/application/repository_node_install_test.go @@ -0,0 +1,23 @@ +package application + +import ( + "testing" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/tmsclient" +) + +func TestRepositoryNodeInstallResultUncertain(t *testing.T) { + detail := tmsclient.TaskDetail{Targets: []tmsclient.TargetResult{{ + AgentID: 99, Status: "FAILED", ErrorMessage: "sent_ack_timeout", + }}} + if !repositoryNodeInstallResultUncertain(detail, 99) { + t.Fatal("sent ACK timeout must leave enrollment pending for a possible delayed bootstrap") + } + if repositoryNodeInstallResultUncertain(detail, 100) { + t.Fatal("another target must not affect this enrollment") + } + detail.Targets[0].ErrorMessage = "artifact checksum mismatch" + if repositoryNodeInstallResultUncertain(detail, 99) { + t.Fatal("a deterministic execution failure must remain terminal") + } +} diff --git a/manager/backend/services/footstone/application/repository_node_internal.go b/manager/backend/services/footstone/application/repository_node_internal.go new file mode 100644 index 0000000000000000000000000000000000000000..b9cf7edba865b2bd5896c07d17eff95cada7aba9 --- /dev/null +++ b/manager/backend/services/footstone/application/repository_node_internal.go @@ -0,0 +1,449 @@ +package application + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/google/uuid" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryTaskService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/nodeprotocol" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/taskresult" +) + +var ( + ErrRepositoryNodeOffline = errors.New("REPOSITORY_NODE_OFFLINE") + ErrRepositoryNodeUnauthorized = errors.New("REPOSITORY_NODE_UNAUTHORIZED") + ErrRepositoryNodeDataUnavailable = errors.New("REPOSITORY_NODE_DATA_UNAVAILABLE") +) + +func (a *application) HeartbeatRepositoryStorageNode(input request.RepositoryStorageNodeHeartbeat) ( + *po.RepositoryStorageNode, error, +) { + node, err := a.repositoryNodeSrv.RecordHeartbeat(input) + if err != nil { + return nil, err + } + // Remote workers do not access MySQL, so lease recovery stays in the control plane. + if _, err = a.repositoryTaskSrv.RecoverExpired(); err != nil { + return nil, fmt.Errorf("recover expired repository tasks: %w", err) + } + return node, nil +} + +func (a *application) AuthenticateRepositoryStorageNode(storageNodeID, certificateFingerprint string) error { + node, err := a.repositoryNodeSrv.AuthenticateInternal(storageNodeID, certificateFingerprint) + if err != nil || !node.Enabled || node.Status == request.RepositoryStorageNodeStatusDisabled { + return ErrRepositoryNodeUnauthorized + } + return nil +} + +func (a *application) BootstrapRepositoryStorageNode(input request.RepositoryNodeBootstrapRequest) ( + *request.RepositoryNodeCertificateBundle, error, +) { + return a.repositoryNodeSrv.BootstrapNode(input) +} + +func (a *application) RenewRepositoryStorageNodeCertificate(nodeID string, csrPEM []byte) ( + *request.RepositoryNodeCertificateBundle, error, +) { + return a.repositoryNodeSrv.RenewNodeCertificate(nodeID, csrPEM) +} + +func (a *application) ClaimRepositoryTaskForNode(storageNodeID, workerID string, lease time.Duration) ( + *po.RepositoryTask, *po.SoftwareRepository, error, +) { + node, err := a.repositoryNodeSrv.DescribeInternal(storageNodeID) + if err != nil { + return nil, nil, err + } + if !node.Enabled || node.Status == request.RepositoryStorageNodeStatusDisabled { + return nil, nil, ErrRepositoryNodeUnauthorized + } + if node.Status != request.RepositoryStorageNodeStatusOnline { + return nil, nil, ErrRepositoryNodeOffline + } + task, err := a.repositoryTaskSrv.ClaimNextForNode(storageNodeID, workerID, lease) + if err != nil || task == nil { + return task, nil, err + } + repository, err := a.repositorySrv.DescribeInternal(task.RepositoryID) + if err != nil { + return nil, nil, err + } + return task, repository, nil +} + +func (a *application) RepositoryGPGKeyMetadataForNode(repositoryID string) ([]po.RepositoryGPGKey, error) { + return a.repositoryContentSrv.ListGPGKeyMetadataInternal(repositoryID) +} + +func (a *application) RepositoryMetadataSigningKeyForNode(repository *po.SoftwareRepository) ( + *po.RepositoryMetadataSigningKey, error, +) { + if repository == nil || repository.MetadataSigningKeyRef == "" { + return nil, nil + } + return a.repositoryNodeSrv.MetadataSigningKeyInternal(repository.PrimaryStorageNodeID, + repository.MetadataSigningKeyRef) +} + +func (a *application) HeartbeatRepositoryNodeTask(input request.HeartbeatRepositoryTaskRequest) ( + *po.RepositoryTask, error, +) { + if err := a.authorizeRepositoryTaskNode(input.StorageNodeID); err != nil { + return nil, err + } + return a.repositoryTaskSrv.Heartbeat(input) +} + +func (a *application) ReportRepositoryNodeTaskFailure(ctx context.Context, + input request.CompleteRepositoryTaskRequest, +) ( + *po.RepositoryTask, error, +) { + if err := a.authorizeRepositoryTaskNode(input.StorageNodeID); err != nil { + return nil, err + } + if input.Status != request.RepositoryTaskStatusFailed && input.Status != request.RepositoryTaskStatusCanceled { + return nil, repositoryTaskService.ErrInvalidTaskTransition + } + task, err := a.repositoryTaskSrv.DescribeInternal(input.TaskID) + if err != nil { + return nil, err + } + if task.StorageNodeID != input.StorageNodeID { + return nil, repositoryTaskService.ErrTaskStorageNodeMismatch + } + node, err := a.repositoryNodeSrv.DescribeInternal(input.StorageNodeID) + if err != nil { + return nil, err + } + logMetadata, err := a.readRepositoryTaskLogMetadata(ctx, node, task, input.Status) + if err != nil { + return nil, err + } + if input.ResultObjectID == "" && input.ResultObjectSHA256 == "" && input.QuarantineCount == 0 { + return a.repositoryTaskSrv.CompleteWithMutation(input, func(session entity.Session, _ bool) error { + if _, importErr := a.repositoryNodeSrv.ImportTaskLogIndex(task, logMetadata, session); importErr != nil { + return importErr + } + return a.repositoryContentSrv.MarkUploadFailedFromTask(task, input.ErrorCode, input.ErrorMessage, session) + }) + } + if input.Status != request.RepositoryTaskStatusFailed || a.repositoryNodeResult == nil { + return nil, repositoryTaskService.ErrInvalidTaskTransition + } + if task.Type != request.RepositoryTaskTypeUpload || task.StorageNodeID != input.StorageNodeID { + return nil, repositoryTaskService.ErrInvalidTaskTransition + } + var snapshot request.RepositoryUploadSnapshot + if err = json.Unmarshal([]byte(task.PayloadJSON), &snapshot); err != nil || uuid.Validate(snapshot.UploadID) != nil { + return nil, repositoryTaskService.ErrInvalidTaskTransition + } + content, err := a.repositoryNodeResult.ReadTaskResult(ctx, node, task.ID, + input.ResultObjectID, input.ResultObjectSHA256) + if err != nil { + return nil, err + } + document, err := taskresult.ParseFailureZstd(bytes.NewReader(content), taskresult.FailureExpectation{ + TaskID: task.ID, RepositoryID: task.RepositoryID, StorageNodeID: task.StorageNodeID, + UploadID: snapshot.UploadID, ErrorCode: input.ErrorCode, QuarantineCount: input.QuarantineCount, + }) + if err != nil || document.Failure.ErrorMessage != input.ErrorMessage { + if err != nil { + return nil, err + } + return nil, repositoryTaskService.ErrInvalidTaskTransition + } + quarantine := remoteQuarantinePO(task, document.Quarantine) + return a.repositoryTaskSrv.CompleteWithMutation(input, func(session entity.Session, repeated bool) error { + if _, importErr := a.repositoryNodeSrv.ImportTaskLogIndex(task, logMetadata, session); importErr != nil { + return importErr + } + if !repeated && task.RetryOfTaskID != "" { + if retryErr := a.repositoryContentSrv.PrepareRemoteUploadRetry(task, snapshot, session); retryErr != nil { + return retryErr + } + } + return a.repositoryContentSrv.ImportRemoteUploadFailure(task, snapshot, quarantine, + document.Failure.ErrorCode, document.Failure.ErrorMessage, session) + }) +} + +func (a *application) ExecuteRepositoryDistributionForNode(ctx context.Context, + input request.ExecuteRepositoryDistributionTaskRequest, +) (*po.RepositoryTask, *po.RepositoryDistribution, error) { + if err := a.authorizeRepositoryTaskNode(input.StorageNodeID); err != nil { + return nil, nil, err + } + task, err := a.repositoryTaskSrv.DescribeInternal(input.TaskID) + if err != nil { + return nil, nil, err + } + if task.StorageNodeID != input.StorageNodeID { + return nil, nil, repositoryTaskService.ErrTaskStorageNodeMismatch + } + if task.Type != request.RepositoryTaskTypeDistribute || task.Status != request.RepositoryTaskStatusRunning { + return nil, nil, repositoryTaskService.ErrInvalidTaskTransition + } + if _, err = a.repositoryTaskSrv.Heartbeat(request.HeartbeatRepositoryTaskRequest{ + TaskID: task.ID, StorageNodeID: input.StorageNodeID, WorkerID: input.WorkerID, + FencingToken: input.FencingToken, LeaseSeconds: 60, + CurrentStep: "dispatching immutable repository revision", + }); err != nil { + return nil, nil, err + } + var payload request.RepositoryDistributionPayload + if err = json.Unmarshal([]byte(task.PayloadJSON), &payload); err != nil || + uuid.Validate(payload.DistributionID) != nil { + return nil, nil, repositoryTaskService.ErrInvalidTaskTransition + } + outcome := a.repositoryDistributionWorker.Handle(ctx, task) + completed, err := a.repositoryTaskSrv.Complete(request.CompleteRepositoryTaskRequest{ + TaskID: task.ID, StorageNodeID: input.StorageNodeID, WorkerID: input.WorkerID, + FencingToken: input.FencingToken, Status: outcome.Status, + ErrorCode: outcome.ErrorCode, ErrorMessage: outcome.ErrorMessage, + }) + if err != nil { + return nil, nil, err + } + distribution, err := a.repositoryDistributionSrv.DescribeInternal(payload.DistributionID) + if err != nil { + return nil, nil, err + } + return completed, distribution, nil +} + +func (a *application) PrepareRepositoryNodeTaskCommit(ctx context.Context, + input request.PrepareRepositoryTaskCommitRequest, +) ( + *po.RepositoryTask, error, +) { + if err := a.authorizeRepositoryTaskNode(input.StorageNodeID); err != nil { + return nil, err + } + if a.repositoryNodeResult == nil { + return nil, ErrRepositoryNodeDataUnavailable + } + task, err := a.repositoryTaskSrv.DescribeInternal(input.TaskID) + if err != nil { + return nil, err + } + if task.StorageNodeID != input.StorageNodeID { + return nil, repositoryTaskService.ErrTaskStorageNodeMismatch + } + node, err := a.repositoryNodeSrv.DescribeInternal(input.StorageNodeID) + if err != nil { + return nil, err + } + logMetadata, err := a.readRepositoryTaskLogMetadata(ctx, node, task, input.CompletionStatus) + if err != nil { + return nil, err + } + repository, err := a.repositorySrv.DescribeInternal(task.RepositoryID) + if err != nil { + return nil, err + } + content, err := a.repositoryNodeResult.ReadTaskResult(ctx, node, input.TaskID, + input.ResultObjectID, input.ResultObjectSHA256) + if err != nil { + return nil, err + } + revisionKind := revisionKindForTask(task.Type) + var migrationSnapshot request.RepositoryMigrationSnapshot + if task.Type == request.RepositoryTaskTypeMigrateRepository { + if err = json.Unmarshal([]byte(task.PayloadJSON), &migrationSnapshot); err != nil || + uuid.Validate(migrationSnapshot.RevisionID) != nil || migrationSnapshot.RevisionID != task.RevisionID { + return nil, repositoryTaskService.ErrInvalidTaskTransition + } + revisionKind = migrationSnapshot.RevisionKind + } + if revisionKind == "" { + return nil, repositoryTaskService.ErrInvalidTaskTransition + } + var uploadSnapshot request.RepositoryUploadSnapshot + if task.Type == request.RepositoryTaskTypeUpload { + if err = json.Unmarshal([]byte(task.PayloadJSON), &uploadSnapshot); err != nil || + uuid.Validate(uploadSnapshot.UploadID) != nil { + return nil, repositoryTaskService.ErrInvalidTaskTransition + } + } + document, err := taskresult.ParseZstd(bytes.NewReader(content), taskresult.Expectation{ + TaskID: task.ID, RepositoryID: task.RepositoryID, StorageNodeID: task.StorageNodeID, + RevisionID: input.RevisionID, PublishKey: repository.PublishKey, + ManifestSHA256: input.ManifestSHA256, Kind: revisionKind, + Architecture: repository.Architecture, PackageCount: input.PackageCount, + TotalBytes: input.TotalBytes, MaxPackages: a.appConf.Application.Repository.WithDefaults().MaxSyncPackages, + CompletionStatus: input.CompletionStatus, UploadID: uploadSnapshot.UploadID, + }) + if err != nil { + return nil, err + } + revision, packages, quarantine := remoteResultPO(task, document) + return a.repositoryTaskSrv.PrepareCommitWithMutation(input, func(session entity.Session, repeated bool) error { + if _, importErr := a.repositoryNodeSrv.ImportTaskLogIndex(task, logMetadata, session); importErr != nil { + return importErr + } + if task.Type == request.RepositoryTaskTypeMigrateRepository { + if len(quarantine) != 0 || document.Revision.CompletionStatus != request.RepositoryTaskStatusSuccess { + return repositoryTaskService.ErrInvalidTaskTransition + } + return a.repositoryContentSrv.ValidateRemoteMigration(task, migrationSnapshot, + revision, packages, session) + } + if !repeated && task.Type == request.RepositoryTaskTypeUpload && task.RetryOfTaskID != "" { + if retryErr := a.repositoryContentSrv.PrepareRemoteUploadRetry(task, uploadSnapshot, session); retryErr != nil { + return retryErr + } + } + if importErr := a.repositoryContentSrv.ImportRemoteRevision(revision, packages, quarantine, session); importErr != nil { + return importErr + } + if task.Type == request.RepositoryTaskTypeUpload { + return a.repositoryContentSrv.ApplyRemoteUploadOutcome(task, uploadSnapshot, + document.Revision.CompletionStatus, document.Revision.ErrorCode, + document.Revision.ErrorMessage, session) + } + return nil + }) +} + +type repositoryNodeTaskLogReader interface { + ReadTaskLogMetadata(context.Context, *po.RepositoryStorageNode, string) (*nodeprotocol.TaskLogMetadata, error) +} + +func (a *application) readRepositoryTaskLogMetadata(ctx context.Context, node *po.RepositoryStorageNode, + task *po.RepositoryTask, expectedStatus string, +) (request.RepositoryTaskLogMetadata, error) { + reader, ok := a.repositoryNodeResult.(repositoryNodeTaskLogReader) + if !ok { + return request.RepositoryTaskLogMetadata{}, ErrRepositoryNodeDataUnavailable + } + value, err := reader.ReadTaskLogMetadata(ctx, node, task.ID) + if err != nil { + return request.RepositoryTaskLogMetadata{}, err + } + if value.CompletionStatus != expectedStatus { + return request.RepositoryTaskLogMetadata{}, repositoryTaskService.ErrInvalidTaskTransition + } + return request.RepositoryTaskLogMetadata{TaskID: value.TaskID, RelativePath: value.RelativePath, + SHA256: value.SHA256, CompressedBytes: value.CompressedBytes, + UncompressedBytes: value.UncompressedBytes, LineCount: value.LineCount, + Summary: value.Summary, ErrorFingerprint: value.ErrorFingerprint, + CompletionStatus: value.CompletionStatus, ExpiresAt: value.ExpiresAt, CreatedAt: value.CreatedAt}, nil +} + +func revisionKindForTask(taskType string) string { + switch taskType { + case request.RepositoryTaskTypeSync: + return request.RepositoryRevisionKindSync + case request.RepositoryTaskTypeUpload: + return request.RepositoryRevisionKindUpload + case request.RepositoryTaskTypeDeletePackage: + return request.RepositoryRevisionKindDeletePackage + default: + return "" + } +} + +func remoteResultPO(task *po.RepositoryTask, document *taskresult.Document) (*po.RepositoryRevision, + []po.RepositoryPackage, []po.RepositoryQuarantineItem, +) { + now := time.Now().UTC() + header := document.Revision + revision := &po.RepositoryRevision{ID: header.RevisionID, RepositoryID: header.RepositoryID, + StorageNodeID: header.StorageNodeID, ParentRevisionID: header.ParentRevisionID, TaskID: header.TaskID, + Kind: header.Kind, Status: request.RepositoryRevisionStatusPublished, + PackageCount: header.PackageCount, TotalBytes: header.TotalBytes, ManifestSHA256: header.ManifestSHA256, + RepomdSHA256: header.RepomdSHA256, SnapshotManifestSHA256: header.SnapshotManifestSHA256, + MetadataSigned: header.MetadataSigned, MetadataSigningKeyRef: header.MetadataSigningKeyRef, + MetadataSigningFingerprint: header.MetadataSigningFingerprint, + RepomdSignaturePath: header.RepomdSignaturePath, MetadataPublicKeyURL: header.MetadataPublicKeyURL, + RelativePath: header.RelativePath, CreatedBy: task.RequestedBy, PublishedAt: &now} + packages := make([]po.RepositoryPackage, 0, len(document.Packages)) + for _, item := range document.Packages { + packages = append(packages, po.RepositoryPackage{ID: uuid.NewString(), RepositoryID: item.RepositoryID, + RevisionID: item.RevisionID, Name: item.Name, Epoch: item.Epoch, Version: item.Version, + Release: item.Release, Architecture: item.Architecture, NEVRA: item.NEVRA, + FileName: item.FileName, RelativePath: item.RelativePath, SizeBytes: item.SizeBytes, + SHA256: item.SHA256, Source: item.Source, SourceUploadID: item.SourceUploadID, + SignatureStatus: normalizeRemoteSignatureStatus(item.SignatureStatus), + SignerKeyID: item.SignerKeyID, SignatureMessage: item.SignatureMessage}) + } + quarantine := remoteQuarantinePO(task, document.Quarantine) + return revision, packages, quarantine +} + +func normalizeRemoteSignatureStatus(value string) string { + if value == "" { + return request.RepositoryPackageSignatureUnchecked + } + return value +} + +func remoteQuarantinePO(task *po.RepositoryTask, values []taskresult.QuarantineRecord) []po.RepositoryQuarantineItem { + quarantine := make([]po.RepositoryQuarantineItem, 0, len(values)) + for _, item := range values { + quarantine = append(quarantine, po.RepositoryQuarantineItem{ID: uuid.NewSHA1(uuid.MustParse(task.ID), + []byte(item.RelativePath)).String(), RepositoryID: item.RepositoryID, TaskID: item.TaskID, + UploadID: item.UploadID, OriginalName: item.OriginalName, RelativePath: item.RelativePath, + SizeBytes: item.SizeBytes, SHA256: item.SHA256, ReasonCode: item.ReasonCode, + ReasonMessage: item.ReasonMessage, DetectedNEVRA: item.DetectedNEVRA}) + } + return quarantine +} + +func (a *application) ConfirmRepositoryNodeTaskCommit(input request.ConfirmRepositoryTaskCommitRequest) ( + *po.RepositoryTask, error, +) { + if err := a.authorizeRepositoryTaskNode(input.StorageNodeID); err != nil { + return nil, err + } + task, err := a.repositoryTaskSrv.DescribeInternal(input.TaskID) + if err != nil { + return nil, err + } + if task.Type == request.RepositoryTaskTypeDeletePackage { + return a.repositoryTaskSrv.ConfirmAndCompleteCommitWithMutation(input, + func(session entity.Session, value *po.RepositoryTask, _ bool) error { + return a.repositoryContentSrv.CompleteRemotePackageDeletion(value, session) + }) + } + if task.Type != request.RepositoryTaskTypeMigrateRepository { + return a.repositoryTaskSrv.ConfirmAndCompleteCommit(input) + } + return a.repositoryTaskSrv.ConfirmAndCompleteCommitWithMutation(input, + func(session entity.Session, value *po.RepositoryTask, _ bool) error { + var snapshot request.RepositoryMigrationSnapshot + if decodeErr := json.Unmarshal([]byte(value.PayloadJSON), &snapshot); decodeErr != nil { + return repositoryTaskService.ErrInvalidTaskTransition + } + return a.repositoryContentSrv.CompleteRemoteMigration(value, snapshot, session) + }) +} + +func (a *application) authorizeRepositoryTaskNode(storageNodeID string) error { + if storageNodeID == "" { + return ErrRepositoryNodeUnauthorized + } + node, err := a.repositoryNodeSrv.DescribeInternal(storageNodeID) + if err != nil { + return err + } + if !node.Enabled || node.Status == request.RepositoryStorageNodeStatusDisabled { + return ErrRepositoryNodeUnauthorized + } + if node.Status != request.RepositoryStorageNodeStatusOnline { + return ErrRepositoryNodeOffline + } + return nil +} diff --git a/manager/backend/services/footstone/application/repository_node_upload_adapter.go b/manager/backend/services/footstone/application/repository_node_upload_adapter.go new file mode 100644 index 0000000000000000000000000000000000000000..9e322d9badd6921d283df8ca7496514e02240dc1 --- /dev/null +++ b/manager/backend/services/footstone/application/repository_node_upload_adapter.go @@ -0,0 +1,72 @@ +package application + +import ( + "context" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/nodeclient" +) + +type repositoryNodeUploadAdapter struct{ client *nodeclient.Client } + +func (a *repositoryNodeUploadAdapter) BeginUpload(ctx context.Context, node *po.RepositoryStorageNode, + uploadID string, expectedSize int64, +) (string, error) { + return a.client.BeginUpload(ctx, node, uploadID, expectedSize) +} + +func (a *repositoryNodeUploadAdapter) AppendUploadChunk(ctx context.Context, node *po.RepositoryStorageNode, + uploadID string, expectedSize, offset int64, data []byte, +) (int64, error) { + return a.client.AppendUploadChunk(ctx, node, uploadID, expectedSize, offset, data) +} + +func (a *repositoryNodeUploadAdapter) PutUploadChunk(ctx context.Context, node *po.RepositoryStorageNode, + uploadID string, chunkIndex int, data []byte, expectedSHA256 string, +) (string, error) { + return a.client.PutUploadChunk(ctx, node, uploadID, chunkIndex, data, expectedSHA256) +} + +func (a *repositoryNodeUploadAdapter) CompleteUpload(ctx context.Context, node *po.RepositoryStorageNode, + uploadID string, expectedSize int64, expectedSHA256 string, totalChunks ...int, +) (string, int64, string, error) { + result, err := a.client.CompleteUpload(ctx, node, uploadID, expectedSize, expectedSHA256, totalChunks...) + if err != nil { + return "", 0, "", err + } + return result.RelativePath, result.SizeBytes, result.SHA256, nil +} + +func (a *repositoryNodeUploadAdapter) AbortUpload(ctx context.Context, node *po.RepositoryStorageNode, + uploadID string, +) error { + return a.client.AbortUpload(ctx, node, uploadID) +} + +func (a *repositoryNodeUploadAdapter) PutGPGPublicKey(ctx context.Context, node *po.RepositoryStorageNode, + repositoryID, publishKey, fingerprint, keyID string, content []byte, +) (string, string, error) { + result, err := a.client.PutGPGPublicKey(ctx, node, repositoryID, publishKey, fingerprint, keyID, content) + if err != nil { + return "", "", err + } + publicURL, err := a.client.VerifyGPGPublicKey(ctx, node, repositoryID, publishKey, + fingerprint, result.PrivatePath) + if err != nil { + _ = a.client.RemoveGPGPublicKey(ctx, node, repositoryID, publishKey, fingerprint) + return "", "", err + } + return result.PrivatePath, publicURL, nil +} + +func (a *repositoryNodeUploadAdapter) VerifyGPGPublicKey(ctx context.Context, node *po.RepositoryStorageNode, + repositoryID, publishKey, fingerprint, expectedPrivatePath string, +) (string, error) { + return a.client.VerifyGPGPublicKey(ctx, node, repositoryID, publishKey, fingerprint, expectedPrivatePath) +} + +func (a *repositoryNodeUploadAdapter) RemoveGPGPublicKey(ctx context.Context, node *po.RepositoryStorageNode, + repositoryID, publishKey, fingerprint string, +) error { + return a.client.RemoveGPGPublicKey(ctx, node, repositoryID, publishKey, fingerprint) +} diff --git a/manager/backend/services/footstone/application/repository_ocai.go b/manager/backend/services/footstone/application/repository_ocai.go new file mode 100644 index 0000000000000000000000000000000000000000..7212dc9c622417f9bbbd0b5efbc354be1f43bc68 --- /dev/null +++ b/manager/backend/services/footstone/application/repository_ocai.go @@ -0,0 +1,198 @@ +package application + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "strings" + "unicode/utf8" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryDiagnosisService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryTaskService" +) + +const ( + maximumRepositoryOCAIContextBytes = 32 * 1024 + initialRepositoryLogSummaryBytes = 24 * 1024 +) + +var ( + ErrRepositoryOCAIUnavailable = errors.New("REPOSITORY_OCAI_UNAVAILABLE") + ErrRepositoryOCAIContextChanged = errors.New("repository OCAI context changed after analysis creation") +) + +const repositoryOCAIInstruction = `你是 OCManager 私有化软件仓库的故障分析助手。请只根据下面的结构化上下文分析,不要假设拥有主机或仓库写权限,也不要声称已经执行命令。 +请按“故障分类、最可能原因、关键证据、建议操作、验证方法”的顺序回答。若证据不足,明确列出缺失信息。建议中的重试、重新同步或重建操作必须由用户回到 OCManager 仓库管理页面确认执行。 + +Repository 任务上下文: +` + +func (a *application) BuildRepositoryOCAIContext(ctx entity.Context, taskID string) ( + *request.RepositoryOCAIContext, error, +) { + task, err := a.repositoryTaskSrv.Describe(ctx, taskID) + if err != nil { + return nil, err + } + repository, err := a.repositorySrv.Describe(ctx, task.RepositoryID) + if err != nil { + return nil, err + } + value := &request.RepositoryOCAIContext{ + TaskID: task.ID, RepositoryID: repository.ID, RepositoryName: sanitizeOCAI(repository.Name), + SystemProduct: sanitizeOCAI(repository.SystemProduct), ProductVersion: sanitizeOCAI(repository.ProductVersion), + Architecture: sanitizeOCAI(repository.Architecture), TaskType: sanitizeOCAI(task.Type), + TaskStatus: sanitizeOCAI(task.Status), StorageNodeID: sanitizeOCAI(task.StorageNodeID), + RevisionID: sanitizeOCAI(task.RevisionID), CurrentStep: sanitizeOCAI(task.CurrentStep), + ErrorCode: sanitizeOCAI(task.ErrorCode), ErrorMessage: sanitizeOCAI(task.ErrorMessage), + Diagnosis: sanitizeOCAIDiagnosis(task.StructuredDiagnosis()), + } + if index, indexErr := a.repositoryNodeSrv.GetTaskLogIndexInternal(task.ID); indexErr == nil { + value.LogSummary, value.Truncated = truncateUTF8(sanitizeOCAI(index.Summary), initialRepositoryLogSummaryBytes) + } + if err = finalizeRepositoryOCAIContext(value); err != nil { + return nil, err + } + return value, nil +} + +func (a *application) CreateRepositoryOCAIAnalysis(ctx entity.Context, taskID string) ( + *request.RepositoryOCAIAnalysisResult, error, +) { + contextValue, err := a.BuildRepositoryOCAIContext(ctx, taskID) + if err != nil { + return nil, err + } + analysis, err := a.repositoryAISrv.Create(ctx, taskID, contextValue.ContextSHA256) + if err != nil { + return nil, err + } + return repositoryOCAIResult(analysis, contextValue), nil +} + +func (a *application) DescribeRepositoryOCAIAnalysis(ctx entity.Context, analysisID string) ( + *request.RepositoryOCAIAnalysisResult, error, +) { + analysis, err := a.repositoryAISrv.Describe(analysisID) + if err != nil { + return nil, err + } + contextValue, err := a.BuildRepositoryOCAIContext(ctx, analysis.TaskID) + if err != nil { + return nil, err + } + if contextValue.ContextSHA256 != analysis.ContextSHA256 { + return nil, ErrRepositoryOCAIContextChanged + } + return repositoryOCAIResult(analysis, contextValue), nil +} + +func (a *application) AppendRepositoryOCAIFeedback(ctx entity.Context, sessionID, actionTaskID, actionResult string) ( + *request.RepositoryOCAIAnalysisResult, error, +) { + analysis, err := a.repositoryAISrv.DescribeBySessionID(sessionID) + if err != nil { + return nil, err + } + if _, err = a.repositoryTaskSrv.Describe(ctx, analysis.TaskID); err != nil { + return nil, err + } + actionTask, err := a.repositoryTaskSrv.Describe(ctx, actionTaskID) + if err != nil { + return nil, err + } + if actionTask.RepositoryID == "" { + return nil, repositoryTaskService.ErrTaskNotRunnable + } + originalTask, err := a.repositoryTaskSrv.Describe(ctx, analysis.TaskID) + if err != nil { + return nil, err + } + if actionTask.RepositoryID != originalTask.RepositoryID { + return nil, fmt.Errorf("repository OCAI feedback task belongs to another repository") + } + analysis, err = a.repositoryAISrv.AppendFeedback(sessionID, actionTaskID, actionResult) + if err != nil { + return nil, err + } + contextValue, err := a.BuildRepositoryOCAIContext(ctx, analysis.TaskID) + if err != nil { + return nil, err + } + return repositoryOCAIResult(analysis, contextValue), nil +} + +func repositoryOCAIResult(analysis *po.RepositoryAIAnalysis, + contextValue *request.RepositoryOCAIContext, +) *request.RepositoryOCAIAnalysisResult { + return &request.RepositoryOCAIAnalysisResult{ + Analysis: analysis, Context: contextValue, + Instruction: repositoryOCAIInstruction + contextValue.ContextJSON, + } +} + +func finalizeRepositoryOCAIContext(value *request.RepositoryOCAIContext) error { + for { + content, err := json.Marshal(value) + if err != nil { + return fmt.Errorf("encode repository OCAI context: %w", err) + } + if len(content) <= maximumRepositoryOCAIContextBytes { + digest := sha256.Sum256(content) + value.ContextJSON = string(content) + value.ContextSHA256 = hex.EncodeToString(digest[:]) + return nil + } + if value.LogSummary == "" { + return fmt.Errorf("repository OCAI context exceeds %d bytes", maximumRepositoryOCAIContextBytes) + } + overflow := len(content) - maximumRepositoryOCAIContextBytes + limit := len(value.LogSummary) - overflow - 256 + if limit < 0 { + limit = 0 + } + value.LogSummary, _ = truncateUTF8(value.LogSummary, limit) + value.Truncated = true + } +} + +func sanitizeOCAI(value string) string { + return repositoryDiagnosisService.Sanitize(strings.TrimSpace(value)) +} + +func sanitizeOCAIDiagnosis(value *po.RepositoryDiagnosis) *po.RepositoryDiagnosis { + if value == nil { + return nil + } + result := *value + result.FailureCategory = sanitizeOCAI(result.FailureCategory) + result.ErrorCode = sanitizeOCAI(result.ErrorCode) + result.FailedStep = sanitizeOCAI(result.FailedStep) + result.LikelyCause = sanitizeOCAI(result.LikelyCause) + result.RecommendedAction = sanitizeOCAI(result.RecommendedAction) + result.RuleID = sanitizeOCAI(result.RuleID) + result.Evidence = make([]string, 0, len(value.Evidence)) + for _, item := range value.Evidence { + result.Evidence = append(result.Evidence, sanitizeOCAI(item)) + } + return &result +} + +func truncateUTF8(value string, maximum int) (string, bool) { + if maximum < 0 { + maximum = 0 + } + if len(value) <= maximum { + return value, false + } + value = value[:maximum] + for !utf8.ValidString(value) && len(value) > 0 { + value = value[:len(value)-1] + } + return value, true +} diff --git a/manager/backend/services/footstone/application/repository_ocai_test.go b/manager/backend/services/footstone/application/repository_ocai_test.go new file mode 100644 index 0000000000000000000000000000000000000000..544d51ee0d53b56d0ec7ae6e3a6b1d650b7f2e55 --- /dev/null +++ b/manager/backend/services/footstone/application/repository_ocai_test.go @@ -0,0 +1,44 @@ +package application + +import ( + "encoding/json" + "strings" + "testing" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" +) + +func TestFinalizeRepositoryOCAIContextBoundsAndSanitizes(t *testing.T) { + value := &request.RepositoryOCAIContext{ + TaskID: "task-1", RepositoryID: "repository-1", RepositoryName: "private", + ErrorMessage: sanitizeOCAI("https://admin:secret@example.invalid Authorization: Bearer raw-token"), + LogSummary: sanitizeOCAI("api_key=raw-key\n" + strings.Repeat("x", 48*1024)), + } + if err := finalizeRepositoryOCAIContext(value); err != nil { + t.Fatalf("finalize context: %v", err) + } + if len(value.ContextJSON) > maximumRepositoryOCAIContextBytes { + t.Fatalf("context exceeds limit: %d", len(value.ContextJSON)) + } + if !value.Truncated { + t.Fatal("expected large log summary to be truncated") + } + if strings.Contains(value.ContextJSON, "secret") || strings.Contains(value.ContextJSON, "raw-token") || + strings.Contains(value.ContextJSON, "raw-key") { + t.Fatalf("sensitive value leaked into context: %s", value.ContextJSON) + } + if len(value.ContextSHA256) != 64 { + t.Fatalf("unexpected context digest %q", value.ContextSHA256) + } + var decoded map[string]interface{} + if err := json.Unmarshal([]byte(value.ContextJSON), &decoded); err != nil { + t.Fatalf("context is not valid JSON: %v", err) + } +} + +func TestTruncateUTF8PreservesValidText(t *testing.T) { + value, truncated := truncateUTF8("OpenCloudOS 仓库", 14) + if !truncated || !json.Valid([]byte(`"`+value+`"`)) { + t.Fatalf("invalid UTF-8 truncation %q", value) + } +} diff --git a/manager/backend/services/footstone/application/repository_package_install.go b/manager/backend/services/footstone/application/repository_package_install.go new file mode 100644 index 0000000000000000000000000000000000000000..696c068438828848b08fc040053071158c23ba0d --- /dev/null +++ b/manager/backend/services/footstone/application/repository_package_install.go @@ -0,0 +1,51 @@ +package application + +import ( + "fmt" + "strconv" + "strings" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryPackageInstallService" +) + +func (a *application) PreviewRepositoryPackageInstall(ctx entity.Context, + req *repositorypb.PreviewRepositoryPackageInstallRequest, +) (*repositoryPackageInstallService.PreviewResult, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + agentIDs := make([]uint64, 0, len(req.GetTargetAgentIds())) + for _, value := range req.GetTargetAgentIds() { + agentID, err := strconv.ParseUint(strings.TrimSpace(value), 10, 64) + if err != nil || agentID == 0 { + return nil, fmt.Errorf("target_agent_ids contains an invalid TMS Agent ID") + } + agentIDs = append(agentIDs, agentID) + } + return a.repositoryPackageInstallSrv.Preview(ctx, request.PreviewRepositoryPackageInstallRequest{ + RepositoryID: req.GetRepositoryId(), RevisionID: req.GetRevisionId(), + PackageNames: req.GetPackageNames(), TargetAgentIDs: agentIDs, + TimeoutSeconds: int(req.GetTimeoutSeconds()), + }) +} + +func (a *application) ConfirmRepositoryPackageInstall(ctx entity.Context, + req *repositorypb.ConfirmRepositoryPackageInstallRequest, +) (*repositoryPackageInstallService.ConfirmResult, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + return a.repositoryPackageInstallSrv.Confirm(ctx, request.ConfirmRepositoryPackageInstallRequest{ + PreviewID: req.GetPreviewId(), PreviewDigest: req.GetPreviewDigest(), + }) +} + +func (a *application) DescribeRepositoryPackageInstall(ctx entity.Context, id string) ( + *po.RepositoryPackageInstall, error, +) { + return a.repositoryPackageInstallSrv.Describe(ctx, id) +} diff --git a/manager/backend/services/footstone/application/repository_report.go b/manager/backend/services/footstone/application/repository_report.go new file mode 100644 index 0000000000000000000000000000000000000000..bb7205d995ae5b4b9567db06f431a0d445706e48 --- /dev/null +++ b/manager/backend/services/footstone/application/repository_report.go @@ -0,0 +1,87 @@ +package application + +import ( + "fmt" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryInventoryService" +) + +func (a *application) ExportRepositoryInventory(ctx entity.Context, + req *repositorypb.ExportRepositoryInventoryRequest, +) (*request.RepositoryExport, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + return a.repositoryInventorySrv.Export(ctx, req.GetProjectId()) +} + +func (a *application) PreviewRepositoryInventoryImport(ctx entity.Context, + req *repositorypb.PreviewRepositoryInventoryImportRequest, +) (*request.RepositoryInventoryImportPreview, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + nodeMappings := make(map[string]string, len(req.GetStorageNodeMappings())) + for _, item := range req.GetStorageNodeMappings() { + if item != nil { + nodeMappings[item.GetSourceRef()] = item.GetTargetStorageNodeId() + } + } + keyMappings := make(map[string]string, len(req.GetSigningKeyMappings())) + for _, item := range req.GetSigningKeyMappings() { + if item != nil { + keyMappings[item.GetSourceKeyRef()] = item.GetTargetKeyRef() + } + } + return a.repositoryInventorySrv.Preview(ctx, request.PreviewRepositoryInventoryImportRequest{ + ProjectID: req.GetProjectId(), Content: req.GetContent(), + ConflictStrategy: repositoryInventoryConflictStrategyFromPB(req.GetConflictStrategy()), + DefaultStorageNodeID: req.GetDefaultStorageNodeId(), + StorageNodeMappings: nodeMappings, SigningKeyMappings: keyMappings, + }) +} + +func (a *application) ConfirmRepositoryInventoryImport(ctx entity.Context, + req *repositorypb.ConfirmRepositoryInventoryImportRequest, +) (*repositoryInventoryService.ConfirmResult, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + return a.repositoryInventorySrv.Confirm(ctx, request.ConfirmRepositoryInventoryImportRequest{ + PreviewID: req.GetPreviewId(), PreviewDigest: req.GetPreviewDigest(), + }) +} + +func repositoryInventoryConflictStrategyFromPB(value repositorypb.RepositoryInventoryConflictStrategy) string { + switch value { + case repositorypb.RepositoryInventoryConflictStrategy_REPOSITORY_INVENTORY_CONFLICT_STRATEGY_FAIL: + return request.RepositoryInventoryConflictFail + case repositorypb.RepositoryInventoryConflictStrategy_REPOSITORY_INVENTORY_CONFLICT_STRATEGY_SKIP: + return request.RepositoryInventoryConflictSkip + case repositorypb.RepositoryInventoryConflictStrategy_REPOSITORY_INVENTORY_CONFLICT_STRATEGY_RENAME: + return request.RepositoryInventoryConflictRename + default: + return "" + } +} + +func (a *application) ExportRepositoryValidationReport(ctx entity.Context, + req *repositorypb.ExportRepositoryValidationReportRequest, +) (*request.RepositoryExport, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + return a.repositoryContentSrv.ExportValidationReport(ctx, req.GetRepositoryId()) +} + +func (a *application) ExportRepositoryDistributionResult(ctx entity.Context, + req *repositorypb.ExportRepositoryDistributionResultRequest, +) (*request.RepositoryExport, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + return a.repositoryDistributionSrv.Export(ctx, req.GetDistributionId()) +} diff --git a/manager/backend/services/footstone/application/repository_schedule.go b/manager/backend/services/footstone/application/repository_schedule.go new file mode 100644 index 0000000000000000000000000000000000000000..b9e96e6053008d4e00da6007a5f1f3b9116cb2ac --- /dev/null +++ b/manager/backend/services/footstone/application/repository_schedule.go @@ -0,0 +1,53 @@ +package application + +import ( + "fmt" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +func (a *application) UpsertRepositorySyncSchedule(ctx entity.Context, + req *repositorypb.UpsertRepositorySyncScheduleRequest, +) (*po.RepositorySyncSchedule, error) { + if req == nil || req.GetSpec() == nil { + return nil, fmt.Errorf("schedule spec is required") + } + spec := req.GetSpec() + return a.repositoryScheduleSrv.Upsert(ctx, request.RepositorySyncScheduleSpec{ + RepositoryID: req.GetRepositoryId(), Cadence: scheduleCadenceFromPB(spec.GetCadence()), + Timezone: spec.GetTimezone(), DayOfWeek: int(spec.GetDayOfWeek()), Hour: int(spec.GetHour()), + Minute: int(spec.GetMinute()), FailureThreshold: int(spec.GetFailureThreshold()), + }) +} + +func (a *application) DescribeRepositorySyncSchedule(ctx entity.Context, repositoryID string) ( + *po.RepositorySyncSchedule, error, +) { + return a.repositoryScheduleSrv.Describe(ctx, repositoryID) +} + +func (a *application) PauseRepositorySyncSchedule(ctx entity.Context, repositoryID string) ( + *po.RepositorySyncSchedule, error, +) { + return a.repositoryScheduleSrv.SetStatus(ctx, repositoryID, request.RepositoryScheduleStatusPaused) +} + +func (a *application) ResumeRepositorySyncSchedule(ctx entity.Context, repositoryID string) ( + *po.RepositorySyncSchedule, error, +) { + return a.repositoryScheduleSrv.SetStatus(ctx, repositoryID, request.RepositoryScheduleStatusEnabled) +} + +func scheduleCadenceFromPB(value repositorypb.RepositorySyncScheduleCadence) string { + switch value { + case repositorypb.RepositorySyncScheduleCadence_REPOSITORY_SYNC_SCHEDULE_CADENCE_DAILY: + return request.RepositoryScheduleCadenceDaily + case repositorypb.RepositorySyncScheduleCadence_REPOSITORY_SYNC_SCHEDULE_CADENCE_WEEKLY: + return request.RepositoryScheduleCadenceWeekly + default: + return "" + } +} diff --git a/manager/backend/services/footstone/application/repository_task.go b/manager/backend/services/footstone/application/repository_task.go new file mode 100644 index 0000000000000000000000000000000000000000..ca6c87b02ff5e3a57210b62206e1161934a2bd5a --- /dev/null +++ b/manager/backend/services/footstone/application/repository_task.go @@ -0,0 +1,93 @@ +package application + +import ( + "fmt" + "strings" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +func (a *application) DescribeRepositoryTask(ctx entity.Context, taskID string) (*po.RepositoryTask, error) { + return a.repositoryTaskSrv.Describe(ctx, taskID) +} + +func (a *application) ListRepositoryTasks(ctx entity.Context, req *repositorypb.ListRepositoryTasksRequest) ( + []po.RepositoryTask, request.PageResponse, request.RepositoryTaskStatistics, error, +) { + if req == nil { + return nil, request.PageResponse{}, request.RepositoryTaskStatistics{}, fmt.Errorf("request is required") + } + filter := request.ListRepositoryTasksRequest{ + ProjectID: req.GetProjectId(), + RepositoryID: req.GetRepositoryId(), + StorageNodeID: req.GetStorageNodeId(), + Type: taskTypeFromPB(req.GetType()), + Status: taskStatusFromPB(req.GetStatus()), + Trigger: req.GetTrigger(), + PageRequest: request.ToPageRequest(req.GetPageRequest()), + } + if value := req.GetCreatedAtFrom(); value != nil { + if err := value.CheckValid(); err != nil { + return nil, request.PageResponse{}, request.RepositoryTaskStatistics{}, + fmt.Errorf("created_at_from is invalid: %w", err) + } + createdAtFrom := value.AsTime().UTC() + filter.CreatedAtFrom = &createdAtFrom + } + if value := req.GetCreatedAtTo(); value != nil { + if err := value.CheckValid(); err != nil { + return nil, request.PageResponse{}, request.RepositoryTaskStatistics{}, + fmt.Errorf("created_at_to is invalid: %w", err) + } + createdAtTo := value.AsTime().UTC() + filter.CreatedAtTo = &createdAtTo + } + return a.repositoryTaskSrv.ListWithStatistics(ctx, filter) +} + +func taskTypeFromPB(value repositorypb.RepositoryTaskType) string { + if value == repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_UNSPECIFIED { + return "" + } + return strings.TrimPrefix(value.String(), "REPOSITORY_TASK_TYPE_") +} + +func (a *application) CancelRepositoryTask(ctx entity.Context, taskID string) (*po.RepositoryTask, error) { + return a.repositoryTaskSrv.Cancel(ctx, taskID) +} + +func (a *application) RetryRepositoryTask(ctx entity.Context, taskID string) (*po.RepositoryTask, error) { + return a.repositoryTaskSrv.Retry(ctx, taskID) +} + +func (a *application) ExportRepositoryTaskDiagnosis(ctx entity.Context, taskID string) ( + *request.RepositoryDiagnosisExport, error, +) { + return a.repositoryTaskSrv.ExportDiagnosis(ctx, taskID) +} + +func taskStatusFromPB(value repositorypb.RepositoryTaskStatus) string { + switch value { + case repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_UNSPECIFIED: + return "" + case repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_PENDING: + return request.RepositoryTaskStatusPending + case repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_RUNNING: + return request.RepositoryTaskStatusRunning + case repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_CANCEL_REQUESTED: + return request.RepositoryTaskStatusCancelRequested + case repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_CANCELED: + return request.RepositoryTaskStatusCanceled + case repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_SUCCESS: + return request.RepositoryTaskStatusSuccess + case repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_FAILED: + return request.RepositoryTaskStatusFailed + case repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_PARTIAL_SUCCESS: + return request.RepositoryTaskStatusPartialSuccess + default: + return strings.TrimPrefix(value.String(), "REPOSITORY_TASK_STATUS_") + } +} diff --git a/manager/backend/services/footstone/application/repository_task_log.go b/manager/backend/services/footstone/application/repository_task_log.go new file mode 100644 index 0000000000000000000000000000000000000000..73e7b9911310ce23d486401a868aaf217b8f98d4 --- /dev/null +++ b/manager/backend/services/footstone/application/repository_task_log.go @@ -0,0 +1,347 @@ +package application + +import ( + "archive/tar" + "bufio" + "compress/gzip" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "sort" + "strings" + "time" + + "github.com/klauspost/compress/zstd" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryErrorCatalog" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/tasklog" +) + +var ErrRepositoryTaskLogExpired = errors.New("REPOSITORY_TASK_LOG_EXPIRED") + +type repositoryNodeTaskLogDownloader interface { + OpenTaskLog(context.Context, *po.RepositoryStorageNode, string, string, string, int64) (io.ReadCloser, error) +} + +func (a *application) DescribeRepositoryTaskLog(ctx entity.Context, taskID string) ( + *po.RepositoryTaskLogIndex, error, +) { + if _, err := a.repositoryTaskSrv.Describe(ctx, taskID); err != nil { + return nil, err + } + value, err := a.repositoryNodeSrv.GetTaskLogIndexInternal(taskID) + if err != nil { + return nil, err + } + if value.DeletedAt != nil || !value.ExpiresAt.After(time.Now().UTC()) { + return nil, ErrRepositoryTaskLogExpired + } + return value, nil +} + +func (a *application) DownloadRepositoryTaskLog(ctx entity.Context, taskID string) ( + *request.RepositoryTaskLogDownload, error, +) { + index, err := a.DescribeRepositoryTaskLog(ctx, taskID) + if err != nil { + return nil, err + } + task, err := a.repositoryTaskSrv.Describe(ctx, taskID) + if err != nil { + return nil, err + } + node, err := a.repositoryNodeSrv.DescribeInternal(task.StorageNodeID) + if err != nil { + return nil, err + } + downloader, ok := a.repositoryNodeResult.(repositoryNodeTaskLogDownloader) + if !ok { + return nil, ErrRepositoryNodeDataUnavailable + } + content, err := downloader.OpenTaskLog(ctx.Context, node, task.ID, index.RelativePath, + index.SHA256, index.CompressedBytes) + if err != nil { + return nil, err + } + defer content.Close() + return buildRepositoryTaskLogBundle(task, index, content) +} + +type repositoryTaskLogContext struct { + TaskID string `json:"task_id"` + RepositoryID string `json:"repository_id,omitempty"` + RepositoryName string `json:"repository_name,omitempty"` + StorageNodeID string `json:"storage_node_id,omitempty"` + StorageNodeName string `json:"storage_node_name,omitempty"` + TaskType string `json:"task_type"` + Trigger string `json:"trigger"` + RevisionID string `json:"revision_id,omitempty"` + CreatedAt string `json:"created_at"` +} + +type repositoryTaskTargetSummary struct { + TargetType string `json:"target_type"` + TargetID string `json:"target_id"` + StorageNodeID string `json:"storage_node_id,omitempty"` + TMSAgentID uint64 `json:"tms_agent_id,omitempty"` + Status string `json:"status"` + ProgressPercent int32 `json:"progress_percent"` + ErrorCode string `json:"error_code,omitempty"` + ErrorMessage string `json:"error_message,omitempty"` + ErrorZhMessage string `json:"error_zh_message,omitempty"` + ErrorZhAction string `json:"error_zh_recommended_action,omitempty"` +} + +type repositoryTaskResultSummary struct { + TaskID string `json:"task_id"` + Status string `json:"status"` + ProgressPercent int32 `json:"progress_percent"` + CurrentStep string `json:"current_step,omitempty"` + ErrorCode string `json:"error_code,omitempty"` + ErrorMessage string `json:"error_message,omitempty"` + ErrorZhMessage string `json:"error_zh_message,omitempty"` + ErrorZhAction string `json:"error_zh_recommended_action,omitempty"` + FailureCategory string `json:"failure_category,omitempty"` + LikelyCause string `json:"likely_cause,omitempty"` + RecommendedAction string `json:"recommended_action,omitempty"` + ErrorFingerprint string `json:"error_fingerprint,omitempty"` + StartedAt string `json:"started_at,omitempty"` + FinishedAt string `json:"finished_at,omitempty"` + Targets []repositoryTaskTargetSummary `json:"targets,omitempty"` +} + +type repositoryTaskBundleFile struct { + name string + path string + content []byte + size int64 + sha256 string +} + +func buildRepositoryTaskLogBundle(task *po.RepositoryTask, index *po.RepositoryTaskLogIndex, + compressed io.Reader, +) (*request.RepositoryTaskLogDownload, error) { + if task == nil || index == nil || compressed == nil { + return nil, errors.New("repository task log bundle input is invalid") + } + logFile, logDigest, logSize, err := redactRepositoryTaskLog(compressed) + if err != nil { + return nil, err + } + defer func() { + _ = os.Remove(logFile) + }() + + contextContent, err := json.MarshalIndent(repositoryTaskLogContext{ + TaskID: task.ID, RepositoryID: task.RepositoryID, + RepositoryName: tasklog.Redact(task.RepositoryName), StorageNodeID: task.StorageNodeID, + StorageNodeName: tasklog.Redact(task.StorageNodeName), TaskType: task.Type, + Trigger: task.Trigger, RevisionID: task.RevisionID, CreatedAt: task.CreatedAt.UTC().Format(time.RFC3339Nano), + }, "", " ") + if err != nil { + return nil, fmt.Errorf("encode repository task log context: %w", err) + } + contextContent = append(contextContent, '\n') + resultContent, err := json.MarshalIndent(repositoryTaskResult(task, index), "", " ") + if err != nil { + return nil, fmt.Errorf("encode repository task result summary: %w", err) + } + resultContent = append(resultContent, '\n') + + files := []repositoryTaskBundleFile{ + bundleContentFile("context.json", contextContent), + bundleContentFile("result-summary.json", resultContent), + {name: "task-log.jsonl.zst", path: logFile, size: logSize, sha256: logDigest}, + } + sort.Slice(files, func(left, right int) bool { return files[left].name < files[right].name }) + var checksums strings.Builder + for _, file := range files { + fmt.Fprintf(&checksums, "%s %s\n", file.sha256, file.name) + } + files = append(files, bundleContentFile("SHA256SUMS", []byte(checksums.String()))) + + bundle, err := os.CreateTemp("", "repository-task-log-*.tar.gz") + if err != nil { + return nil, fmt.Errorf("create repository task log bundle: %w", err) + } + bundlePath := bundle.Name() + removeBundle := true + defer func() { + _ = bundle.Close() + if removeBundle { + _ = os.Remove(bundlePath) + } + }() + digest := sha256.New() + gzipWriter := gzip.NewWriter(io.MultiWriter(bundle, digest)) + tarWriter := tar.NewWriter(gzipWriter) + for _, file := range files { + if err = writeRepositoryTaskBundleFile(tarWriter, file, task.CreatedAt); err != nil { + return nil, err + } + } + if err = tarWriter.Close(); err != nil { + return nil, fmt.Errorf("close repository task log tar: %w", err) + } + if err = gzipWriter.Close(); err != nil { + return nil, fmt.Errorf("close repository task log gzip: %w", err) + } + if err = bundle.Close(); err != nil { + return nil, fmt.Errorf("close repository task log bundle: %w", err) + } + info, err := os.Stat(bundlePath) + if err != nil { + return nil, fmt.Errorf("stat repository task log bundle: %w", err) + } + reader, err := os.Open(bundlePath) + if err != nil { + return nil, fmt.Errorf("open repository task log bundle: %w", err) + } + removeBundle = false + return &request.RepositoryTaskLogDownload{ + FileName: fmt.Sprintf("repository-task-%s-logs.tar.gz", task.ID), + ContentType: "application/gzip", SHA256: hex.EncodeToString(digest.Sum(nil)), + SizeBytes: info.Size(), Content: &removingReadCloser{ReadCloser: reader, path: bundlePath}, + }, nil +} + +func repositoryTaskResult(task *po.RepositoryTask, index *po.RepositoryTaskLogIndex) repositoryTaskResultSummary { + taskError := repositoryTaskBundleError(task.ErrorCode) + result := repositoryTaskResultSummary{ + TaskID: task.ID, Status: task.Status, ProgressPercent: task.ProgressPercent, + CurrentStep: tasklog.Redact(task.CurrentStep), ErrorCode: task.ErrorCode, + ErrorMessage: tasklog.Redact(task.ErrorMessage), FailureCategory: task.FailureCategory, + ErrorZhMessage: taskError.ZhMessage, ErrorZhAction: taskError.ZhRecommendedAction, + LikelyCause: tasklog.Redact(task.LikelyCause), RecommendedAction: tasklog.Redact(task.RecommendedAction), + ErrorFingerprint: index.ErrorFingerprint, + } + if task.StartedAt != nil { + result.StartedAt = task.StartedAt.UTC().Format(time.RFC3339Nano) + } + if task.FinishedAt != nil { + result.FinishedAt = task.FinishedAt.UTC().Format(time.RFC3339Nano) + } + for _, target := range task.Targets { + targetError := repositoryTaskBundleError(target.ErrorCode) + result.Targets = append(result.Targets, repositoryTaskTargetSummary{ + TargetType: target.TargetType, TargetID: tasklog.Redact(target.TargetID), + StorageNodeID: target.StorageNodeID, TMSAgentID: target.TMSAgentID, + Status: target.Status, ProgressPercent: target.ProgressPercent, ErrorCode: target.ErrorCode, + ErrorMessage: tasklog.Redact(target.ErrorMessage), ErrorZhMessage: targetError.ZhMessage, + ErrorZhAction: targetError.ZhRecommendedAction, + }) + } + return result +} + +func repositoryTaskBundleError(code string) repositoryErrorCatalog.Entry { + if strings.TrimSpace(code) == "" { + return repositoryErrorCatalog.Entry{} + } + return repositoryErrorCatalog.Resolve(code) +} + +func redactRepositoryTaskLog(compressed io.Reader) (string, string, int64, error) { + decoder, err := zstd.NewReader(compressed, zstd.WithDecoderMaxMemory(256<<20)) + if err != nil { + return "", "", 0, fmt.Errorf("open repository task log decoder: %w", err) + } + defer decoder.Close() + file, err := os.CreateTemp("", "repository-task-log-*.jsonl.zst") + if err != nil { + return "", "", 0, fmt.Errorf("create redacted repository task log: %w", err) + } + path := file.Name() + remove := true + defer func() { + _ = file.Close() + if remove { + _ = os.Remove(path) + } + }() + digest := sha256.New() + encoder, err := zstd.NewWriter(io.MultiWriter(file, digest), + zstd.WithEncoderLevel(zstd.EncoderLevelFromZstd(3))) + if err != nil { + return "", "", 0, fmt.Errorf("create redacted repository task log encoder: %w", err) + } + writer := bufio.NewWriterSize(encoder, 64<<10) + scanner := bufio.NewScanner(decoder) + scanner.Buffer(make([]byte, 64<<10), 1<<20) + for scanner.Scan() { + line := tasklog.Redact(scanner.Text()) + "\n" + if _, writeErr := writer.WriteString(line); writeErr != nil { + return "", "", 0, fmt.Errorf("write redacted repository task log: %w", writeErr) + } + } + if err = scanner.Err(); err != nil { + return "", "", 0, fmt.Errorf("read repository task log: %w", err) + } + if err = writer.Flush(); err != nil { + return "", "", 0, fmt.Errorf("flush redacted repository task log: %w", err) + } + if err = encoder.Close(); err != nil { + return "", "", 0, fmt.Errorf("close redacted repository task log encoder: %w", err) + } + if err = file.Close(); err != nil { + return "", "", 0, fmt.Errorf("close redacted repository task log: %w", err) + } + info, err := os.Stat(path) + if err != nil { + return "", "", 0, fmt.Errorf("stat redacted repository task log: %w", err) + } + remove = false + return path, hex.EncodeToString(digest.Sum(nil)), info.Size(), nil +} + +func bundleContentFile(name string, content []byte) repositoryTaskBundleFile { + digest := sha256.Sum256(content) + return repositoryTaskBundleFile{name: name, content: content, size: int64(len(content)), sha256: hex.EncodeToString(digest[:])} +} + +func writeRepositoryTaskBundleFile(writer *tar.Writer, file repositoryTaskBundleFile, timestamp time.Time) error { + header := &tar.Header{Name: file.name, Mode: 0600, Size: file.size, ModTime: timestamp.UTC()} + if err := writer.WriteHeader(header); err != nil { + return fmt.Errorf("write repository task bundle header %s: %w", file.name, err) + } + if file.path == "" { + if _, err := writer.Write(file.content); err != nil { + return fmt.Errorf("write repository task bundle content %s: %w", file.name, err) + } + return nil + } + source, err := os.Open(file.path) + if err != nil { + return fmt.Errorf("open repository task bundle source %s: %w", file.name, err) + } + defer source.Close() + if _, err = io.Copy(writer, source); err != nil { + return fmt.Errorf("write repository task bundle source %s: %w", file.name, err) + } + return nil +} + +type removingReadCloser struct { + io.ReadCloser + path string +} + +func (r *removingReadCloser) Close() error { + err := r.ReadCloser.Close() + removeErr := os.Remove(r.path) + if err != nil { + return err + } + if removeErr != nil && !errors.Is(removeErr, os.ErrNotExist) { + return removeErr + } + return nil +} diff --git a/manager/backend/services/footstone/application/repository_task_log_test.go b/manager/backend/services/footstone/application/repository_task_log_test.go new file mode 100644 index 0000000000000000000000000000000000000000..95ca619b2ab5f4e8505658041ad9219dfa639c97 --- /dev/null +++ b/manager/backend/services/footstone/application/repository_task_log_test.go @@ -0,0 +1,125 @@ +package application + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "crypto/sha256" + "encoding/hex" + "io" + "strings" + "testing" + "time" + + "github.com/klauspost/compress/zstd" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +func TestBuildRepositoryTaskLogBundleRedactsAndPackagesEvidence(t *testing.T) { + var compressed bytes.Buffer + encoder, err := zstd.NewWriter(&compressed) + if err != nil { + t.Fatal(err) + } + secret := "raw-secret-value" + if _, err = encoder.Write([]byte("{\"level\":\"ERROR\",\"message\":\"Authorization: Bearer " + secret + + " node_private_key=private-key-material\"}\n")); err != nil { + t.Fatal(err) + } + if err = encoder.Close(); err != nil { + t.Fatal(err) + } + now := time.Date(2026, 9, 29, 12, 0, 0, 0, time.UTC) + task := &po.RepositoryTask{ + ID: "9d8cadff-09b0-48c8-8184-11e1b7bdcdaf", RepositoryID: "repository-id", + RepositoryName: "private-repository", StorageNodeID: "node-id", StorageNodeName: "node-a", + Type: "SYNC", Trigger: "MANUAL", RevisionID: "revision-id", Status: "FAILED", + ProgressPercent: 42, CurrentStep: "download", ErrorCode: "SYNC_DOWNLOAD_FAILED", + ErrorMessage: "password=" + secret, LikelyCause: "上游不可达", + RecommendedAction: "检查网络", CreatedAt: now, StartedAt: &now, FinishedAt: &now, + Targets: []po.RepositoryTaskTarget{{TargetType: "STORAGE_NODE", TargetID: "node-id", + StorageNodeID: "node-id", Status: "FAILED", ErrorCode: "NODE_OFFLINE", + ErrorMessage: "token=" + secret}}, + } + index := &po.RepositoryTaskLogIndex{TaskID: task.ID, ErrorFingerprint: strings.Repeat("a", 64)} + download, err := buildRepositoryTaskLogBundle(task, index, bytes.NewReader(compressed.Bytes())) + if err != nil { + t.Fatal(err) + } + defer download.Content.Close() + if download.ContentType != "application/gzip" || !strings.HasSuffix(download.FileName, "-logs.tar.gz") { + t.Fatalf("download metadata = %#v", download) + } + bundle, err := io.ReadAll(download.Content) + if err != nil { + t.Fatal(err) + } + digest := sha256.Sum256(bundle) + if download.SizeBytes != int64(len(bundle)) || download.SHA256 != hex.EncodeToString(digest[:]) { + t.Fatalf("download size/digest = %d/%s", download.SizeBytes, download.SHA256) + } + gzipReader, err := gzip.NewReader(bytes.NewReader(bundle)) + if err != nil { + t.Fatal(err) + } + archive := tar.NewReader(gzipReader) + files := make(map[string][]byte) + for { + header, nextErr := archive.Next() + if nextErr == io.EOF { + break + } + if nextErr != nil { + t.Fatal(nextErr) + } + content, readErr := io.ReadAll(archive) + if readErr != nil { + t.Fatal(readErr) + } + files[header.Name] = content + } + for _, name := range []string{"context.json", "result-summary.json", "task-log.jsonl.zst", "SHA256SUMS"} { + if len(files[name]) == 0 { + t.Fatalf("bundle missing %s: %#v", name, files) + } + } + for name, content := range files { + if strings.Contains(string(content), secret) || strings.Contains(string(content), "private-key-material") { + t.Fatalf("bundle file %s leaked secret", name) + } + } + logDecoder, err := zstd.NewReader(bytes.NewReader(files["task-log.jsonl.zst"])) + if err != nil { + t.Fatal(err) + } + redactedLog, err := io.ReadAll(logDecoder) + logDecoder.Close() + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(redactedLog), "[REDACTED]") || + !strings.Contains(string(files["result-summary.json"]), "SYNC_DOWNLOAD_FAILED") || + !strings.Contains(string(files["result-summary.json"]), "STORAGE_NODE") || + !strings.Contains(string(files["result-summary.json"]), "error_zh_recommended_action") { + t.Fatalf("bundle evidence is incomplete: %#v", files) + } + checksumLines := strings.Split(strings.TrimSpace(string(files["SHA256SUMS"])), "\n") + if len(checksumLines) != 3 { + t.Fatalf("bundle checksum manifest entries = %d, want 3: %s", len(checksumLines), files["SHA256SUMS"]) + } + for _, line := range checksumLines { + parts := strings.SplitN(line, " ", 2) + if len(parts) != 2 { + t.Fatalf("invalid checksum line %q", line) + } + content, ok := files[parts[1]] + if !ok || parts[1] == "SHA256SUMS" { + t.Fatalf("checksum references unknown file %q", parts[1]) + } + fileDigest := sha256.Sum256(content) + if parts[0] != hex.EncodeToString(fileDigest[:]) { + t.Fatalf("checksum mismatch for %s: %s", parts[1], parts[0]) + } + } +} diff --git a/manager/backend/services/footstone/application/repository_task_test.go b/manager/backend/services/footstone/application/repository_task_test.go new file mode 100644 index 0000000000000000000000000000000000000000..c671cb8314b017d882560b848e96d674904843f9 --- /dev/null +++ b/manager/backend/services/footstone/application/repository_task_test.go @@ -0,0 +1,32 @@ +package application + +import ( + "testing" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" +) + +func TestTaskStatusFromPB(t *testing.T) { + tests := map[repositorypb.RepositoryTaskStatus]string{ + repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_UNSPECIFIED: "", + repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_PENDING: request.RepositoryTaskStatusPending, + repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_RUNNING: request.RepositoryTaskStatusRunning, + repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_CANCEL_REQUESTED: request.RepositoryTaskStatusCancelRequested, + repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_CANCELED: request.RepositoryTaskStatusCanceled, + repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_SUCCESS: request.RepositoryTaskStatusSuccess, + repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_FAILED: request.RepositoryTaskStatusFailed, + repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_PARTIAL_SUCCESS: request.RepositoryTaskStatusPartialSuccess, + } + for input, expected := range tests { + if got := taskStatusFromPB(input); got != expected { + t.Fatalf("taskStatusFromPB(%s) = %q, want %q", input, got, expected) + } + } +} + +func TestTaskStatusFromPBPreservesUnknownForDomainRejection(t *testing.T) { + if got := taskStatusFromPB(repositorypb.RepositoryTaskStatus(99)); got == "" { + t.Fatal("unknown task status became an unfiltered list request") + } +} diff --git a/manager/backend/services/footstone/application/repository_test.go b/manager/backend/services/footstone/application/repository_test.go new file mode 100644 index 0000000000000000000000000000000000000000..f46c7ea9facf96a4a40ef18494fb1cfc553fde78 --- /dev/null +++ b/manager/backend/services/footstone/application/repository_test.go @@ -0,0 +1,51 @@ +package application + +import ( + "testing" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" +) + +func TestRepositorySpecFromPB(t *testing.T) { + spec, err := repositorySpecFromPB(&repositorypb.RepositorySpec{ + ProjectId: 42, + SystemProduct: "OpenCloudOS", + ProductVersion: "9.6", + Name: "private-apps", + Architecture: repositorypb.RepositoryArchitecture_REPOSITORY_ARCHITECTURE_AARCH64, + Type: repositorypb.RepositoryType_REPOSITORY_TYPE_PRIVATE, + AllowUpload: true, + IncludePackages: []string{"ocm-*"}, + ExcludePackages: []string{"*-debug"}, + }) + if err != nil { + t.Fatal(err) + } + if spec.Architecture != request.RepositoryArchitectureARM64 || spec.Type != request.RepositoryTypePrivate { + t.Fatalf("repositorySpecFromPB() architecture/type = %q/%q", spec.Architecture, spec.Type) + } + if spec.GPGPolicy != request.RepositoryGPGDisabled { + t.Fatalf("repositorySpecFromPB() default GPG policy = %q", spec.GPGPolicy) + } + if !spec.Enabled || !spec.AllowUpload || len(spec.IncludePackages) != 1 || len(spec.ExcludePackages) != 1 { + t.Fatalf("repositorySpecFromPB() package settings = %#v", spec) + } +} + +func TestRepositorySpecFromPBPreservesExplicitDisabled(t *testing.T) { + disabled := false + spec, err := repositorySpecFromPB(&repositorypb.RepositorySpec{Enabled: &disabled}) + if err != nil { + t.Fatal(err) + } + if spec.Enabled { + t.Fatal("repositorySpecFromPB() explicit enabled=false became true") + } +} + +func TestRepositorySpecFromPBRequiresSpec(t *testing.T) { + if _, err := repositorySpecFromPB(nil); err == nil { + t.Fatal("repositorySpecFromPB(nil) error = nil") + } +} diff --git a/manager/backend/services/footstone/application/repository_upstream.go b/manager/backend/services/footstone/application/repository_upstream.go new file mode 100644 index 0000000000000000000000000000000000000000..2846cf4c705512a00c97c998b1bfd3b608f64428 --- /dev/null +++ b/manager/backend/services/footstone/application/repository_upstream.go @@ -0,0 +1,81 @@ +package application + +import ( + "fmt" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +func (a *application) CreateRepositoryUpstreamProfile(ctx entity.Context, + req *repositorypb.CreateRepositoryUpstreamProfileRequest, +) (*po.RepositoryUpstreamProfile, error) { + spec, err := upstreamProfileSpecFromPB(req.GetSpec()) + if err != nil { + return nil, err + } + return a.repositoryUpstreamSrv.Create(ctx, spec) +} + +func (a *application) UpdateRepositoryUpstreamProfile(ctx entity.Context, + req *repositorypb.UpdateRepositoryUpstreamProfileRequest, +) (*po.RepositoryUpstreamProfile, error) { + spec, err := upstreamProfileSpecFromPB(req.GetSpec()) + if err != nil { + return nil, err + } + return a.repositoryUpstreamSrv.Update(ctx, req.GetUpstreamProfileId(), spec) +} + +func (a *application) DescribeRepositoryUpstreamProfile(ctx entity.Context, id string) ( + *po.RepositoryUpstreamProfile, error, +) { + return a.repositoryUpstreamSrv.Describe(ctx, id) +} + +func (a *application) ListRepositoryUpstreamProfiles(ctx entity.Context, + req *repositorypb.ListRepositoryUpstreamProfilesRequest, +) ([]po.RepositoryUpstreamProfile, request.PageResponse, error) { + if req == nil { + return nil, request.PageResponse{}, fmt.Errorf("request is required") + } + var enabled *bool + if req.Enabled != nil { + value := req.GetEnabled() + enabled = &value + } + return a.repositoryUpstreamSrv.List(ctx, request.ListRepositoryUpstreamProfilesRequest{ + ProjectID: req.GetProjectId(), SystemProduct: req.GetSystemProduct(), Keyword: req.GetKeyword(), + Enabled: enabled, PageRequest: request.ToPageRequest(req.GetPageRequest()), + }) +} + +func (a *application) DeleteRepositoryUpstreamProfile(ctx entity.Context, id string) error { + return a.repositoryUpstreamSrv.Delete(ctx, id) +} + +func upstreamProfileSpecFromPB(spec *repositorypb.RepositoryUpstreamProfileSpec) ( + request.RepositoryUpstreamProfileSpec, error, +) { + if spec == nil { + return request.RepositoryUpstreamProfileSpec{}, fmt.Errorf("repository upstream profile spec is required") + } + enabled := true + if spec.Enabled != nil { + enabled = spec.GetEnabled() + } + architectures := make([]string, 0, len(spec.GetSupportedArchitectures())) + for _, architecture := range spec.GetSupportedArchitectures() { + architectures = append(architectures, architectureFromPB(architecture)) + } + return request.RepositoryUpstreamProfileSpec{ + ProjectID: spec.GetProjectId(), Name: spec.GetName(), Description: spec.GetDescription(), + SystemProduct: spec.GetSystemProduct(), Channel: spec.GetChannel(), URLTemplate: spec.GetUrlTemplate(), + SupportedVersions: append([]string(nil), spec.GetSupportedVersions()...), + SupportedArchitectures: architectures, + DefaultIncludePackages: append([]string(nil), spec.GetDefaultIncludePackages()...), + DefaultExcludePackages: append([]string(nil), spec.GetDefaultExcludePackages()...), Enabled: enabled, + }, nil +} diff --git a/manager/backend/services/footstone/build/Dockerfile b/manager/backend/services/footstone/build/Dockerfile index e8a51cd65b05cf66cc9bca0c5abcbf4bb6dd6913..b109cdacb7e43dd31621c20e7523f8dde87b53d8 100644 --- a/manager/backend/services/footstone/build/Dockerfile +++ b/manager/backend/services/footstone/build/Dockerfile @@ -1,11 +1,12 @@ -FROM mirror.ccs.tencentyun.com/opencloudos/opencloudos9-minimal:latest +ARG RUNTIME_IMAGE=docker.io/opencloudos/opencloudos9-minimal:9.6-v20260714 +FROM ${RUNTIME_IMAGE} # 复制项目文件 ENV DEST=/home/ocmanager ENV ENTRYCMD=${DEST}/build/start.sh RUN mkdir -p ${DEST}/logs RUN mkdir -p ${DEST}/conf -RUN dnf install -y procps-ng vi telnet curl iproute net-tools tree lrzsz wget +RUN dnf install -y procps-ng vi telnet curl iproute net-tools tree lrzsz wget dnf-plugins-core RUN dnf -y install google-noto-cjk-fonts google-noto-emoji-fonts google-noto-fonts-common google-noto-sans-fonts RUN wget 'https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm' \ && dnf install -y google-chrome-stable_current_x86_64.rpm && dnf clean all diff --git a/manager/backend/services/footstone/configs/application.yaml.template b/manager/backend/services/footstone/configs/application.yaml.template index e3f1e2506da2956941f0e478d383be0a18545239..848e4a31d305ff1a166bb68993d7426a4c7d8bb9 100644 --- a/manager/backend/services/footstone/configs/application.yaml.template +++ b/manager/backend/services/footstone/configs/application.yaml.template @@ -44,6 +44,78 @@ dependence: application: gateway: maxconnection: 1000 + repository: + root: "${REPOSITORY_DATA_ROOT}" + public_base_url: "${REPOSITORY_PUBLIC_BASE_URL}" + tms_base_url: "${TMS}" + tms_user_name: "${REPOSITORY_TMS_USER_NAME}" + max_upload_bytes: ${REPOSITORY_MAX_UPLOAD_BYTES} + max_chunk_bytes: ${REPOSITORY_MAX_CHUNK_BYTES} + max_archive_files: ${REPOSITORY_MAX_ARCHIVE_FILES} + max_expanded_bytes: ${REPOSITORY_MAX_EXPANDED_BYTES} + max_sync_bytes: ${REPOSITORY_MAX_SYNC_BYTES} + max_sync_packages: ${REPOSITORY_MAX_SYNC_PACKAGES} + filtered_sync_bytes: ${REPOSITORY_FILTERED_SYNC_BYTES} + filtered_sync_packages: ${REPOSITORY_FILTERED_SYNC_PACKAGES} + metadata_reserve_bytes: ${REPOSITORY_METADATA_RESERVE_BYTES} + upload_ttl_hours: ${REPOSITORY_UPLOAD_TTL_HOURS} + minimum_free_bytes: ${REPOSITORY_MINIMUM_FREE_BYTES} + metadata_signing_key_root: "${REPOSITORY_METADATA_SIGNING_KEY_ROOT}" + internal_control: + enabled: ${REPOSITORY_NODE_INTERNAL_ENABLED} + listen_address: "${REPOSITORY_NODE_INTERNAL_LISTEN_ADDRESS}" + public_target: "${REPOSITORY_NODE_INTERNAL_PUBLIC_TARGET}" + server_cert: "${REPOSITORY_NODE_INTERNAL_SERVER_CERT}" + server_key: "${REPOSITORY_NODE_INTERNAL_SERVER_KEY}" + server_ca: "${REPOSITORY_NODE_INTERNAL_SERVER_CA}" + server_name: "${REPOSITORY_NODE_INTERNAL_SERVER_NAME}" + client_ca: "${REPOSITORY_NODE_INTERNAL_CLIENT_CA}" + node_bootstrap: + enabled: ${REPOSITORY_NODE_BOOTSTRAP_ENABLED} + listen_address: "${REPOSITORY_NODE_BOOTSTRAP_LISTEN_ADDRESS}" + public_target: "${REPOSITORY_NODE_BOOTSTRAP_PUBLIC_TARGET}" + server_cert: "${REPOSITORY_NODE_BOOTSTRAP_SERVER_CERT}" + server_key: "${REPOSITORY_NODE_BOOTSTRAP_SERVER_KEY}" + server_ca: "${REPOSITORY_NODE_BOOTSTRAP_SERVER_CA}" + server_name: "${REPOSITORY_NODE_BOOTSTRAP_SERVER_NAME}" + signing_ca_cert: "${REPOSITORY_NODE_SIGNING_CA_CERT}" + signing_ca_key: "${REPOSITORY_NODE_SIGNING_CA_KEY}" + certificate_days: ${REPOSITORY_NODE_CERTIFICATE_DAYS} + renew_before_days: ${REPOSITORY_NODE_RENEW_BEFORE_DAYS} + certificate_grace_hours: ${REPOSITORY_NODE_CERTIFICATE_GRACE_HOURS} + enrollment_ttl_minutes: ${REPOSITORY_NODE_ENROLLMENT_TTL_MINUTES} + node_artifact_root: "${REPOSITORY_NODE_ARTIFACT_ROOT}" + node_client: + storage_node_id: "${REPOSITORY_NODE_ID}" + control_target: "${REPOSITORY_NODE_CONTROL_TARGET}" + client_cert: "${REPOSITORY_NODE_CLIENT_CERT}" + client_key: "${REPOSITORY_NODE_CLIENT_KEY}" + server_ca: "${REPOSITORY_NODE_SERVER_CA}" + server_name: "${REPOSITORY_NODE_SERVER_NAME}" + heartbeat_seconds: ${REPOSITORY_NODE_HEARTBEAT_SECONDS} + rpc_timeout_seconds: ${REPOSITORY_NODE_RPC_TIMEOUT_SECONDS} + bootstrap_url: "${REPOSITORY_NODE_BOOTSTRAP_URL}" + bootstrap_server_ca: "${REPOSITORY_NODE_BOOTSTRAP_SERVER_CA}" + bootstrap_server_name: "${REPOSITORY_NODE_BOOTSTRAP_SERVER_NAME}" + bootstrap_token_file: "${REPOSITORY_NODE_BOOTSTRAP_TOKEN_FILE}" + enrollment_id: "${REPOSITORY_NODE_ENROLLMENT_ID}" + install_task_id: "${REPOSITORY_NODE_INSTALL_TASK_ID}" + tms_agent_id: ${REPOSITORY_NODE_TMS_AGENT_ID} + node_server: + enabled: ${REPOSITORY_NODE_DATA_SERVER_ENABLED} + listen_address: "${REPOSITORY_NODE_DATA_SERVER_LISTEN_ADDRESS}" + server_cert: "${REPOSITORY_NODE_DATA_SERVER_CERT}" + server_key: "${REPOSITORY_NODE_DATA_SERVER_KEY}" + client_ca: "${REPOSITORY_NODE_DATA_SERVER_CLIENT_CA}" + max_result_bytes: ${REPOSITORY_NODE_MAX_RESULT_BYTES} + read_timeout_seconds: ${REPOSITORY_NODE_DATA_SERVER_READ_TIMEOUT_SECONDS} + node_data_client: + enabled: ${REPOSITORY_NODE_DATA_CLIENT_ENABLED} + client_cert: "${REPOSITORY_NODE_DATA_CLIENT_CERT}" + client_key: "${REPOSITORY_NODE_DATA_CLIENT_KEY}" + server_ca: "${REPOSITORY_NODE_DATA_CLIENT_SERVER_CA}" + timeout_seconds: ${REPOSITORY_NODE_DATA_CLIENT_TIMEOUT_SECONDS} + max_result_bytes: ${REPOSITORY_NODE_MAX_RESULT_BYTES} stream: batch_store_size: 10000 sys_info: diff --git a/manager/backend/services/footstone/deployments/docker/Dockerfile b/manager/backend/services/footstone/deployments/docker/Dockerfile index 8449f8ee1100821f2c7f9a3cdf08a84b5eda270a..878372eb4ba7da280a0fa415956f1a5d69b58dbb 100644 --- a/manager/backend/services/footstone/deployments/docker/Dockerfile +++ b/manager/backend/services/footstone/deployments/docker/Dockerfile @@ -1,4 +1,5 @@ -FROM mirror.ccs.tencentyun.com/opencloudos/opencloudos9-minimal:latest +ARG RUNTIME_IMAGE=docker.io/opencloudos/opencloudos9-minimal:9.6-v20260714 +FROM ${RUNTIME_IMAGE} # 复制项目文件 ENV DEST=/home/ocmanager diff --git a/manager/backend/services/footstone/domain/auditService/audit.go b/manager/backend/services/footstone/domain/auditService/audit.go index 889830f508ed16f5a4858cefb76f051582282007..b2d0aa725667957203432ac4203edf9aa32c199b 100644 --- a/manager/backend/services/footstone/domain/auditService/audit.go +++ b/manager/backend/services/footstone/domain/auditService/audit.go @@ -205,7 +205,7 @@ func (s AuditService) AddBrowseLog(ctx entity.Context, req *ftpb.AddBrowseLogReq RequestPath: req.RequestPath, PermissionName: permissionName, StaffName: staffName, - HttpBody: req.HttpBody, + HttpBody: auditHTTPBody(req.RequestPath, req.HttpBody), OperationType: inferOperationType(methodName), Date: now, CreatedAt: now, @@ -225,6 +225,20 @@ func (s AuditService) AddBrowseLog(ctx entity.Context, req *ftpb.AddBrowseLogReq return nil } +const maxAuditHTTPBodyBytes = 2048 + +func auditHTTPBody(requestPath, body string) string { + method := (&auditService{}).extractMethodName(requestPath) + if method == "AppendRepositoryUploadChunk" || method == "PutRepositoryUploadChunk" { + return `{"upload_chunk":"omitted from audit log"}` + } + if len(body) <= maxAuditHTTPBodyBytes { + return body + } + const suffix = `...[truncated]` + return body[:maxAuditHTTPBodyBytes-len(suffix)] + suffix +} + // ListBrowseLog impl func (s AuditService) ListBrowseLog(ctx entity.Context, req *ftpb.ListBrowseLogRequest) ( *ftpb.ListBrowseLogResponse, error) { diff --git a/manager/backend/services/footstone/domain/auditService/audit_op_test.go b/manager/backend/services/footstone/domain/auditService/audit_op_test.go index 8822a38af07c53044ac6c9eeda075c6797c762a7..eb6494871cceaee25a605bcb7ea06948916cc5c8 100644 --- a/manager/backend/services/footstone/domain/auditService/audit_op_test.go +++ b/manager/backend/services/footstone/domain/auditService/audit_op_test.go @@ -154,3 +154,21 @@ func TestAuditServiceCountsConcurrentDroppedLogs(t *testing.T) { t.Fatalf("dropped raw logs = %d, want %d", got, calls) } } + +func TestAuditHTTPBodyOmitsUploadChunkAndBoundsOtherRequests(t *testing.T) { + chunk := auditHTTPBody( + "/trpc.ocmanager.repository.RepositoryService/PutRepositoryUploadChunk", + `{"data":"large-base64-body"}`, + ) + if chunk != `{"upload_chunk":"omitted from audit log"}` { + t.Fatalf("upload chunk audit body = %q", chunk) + } + large := make([]byte, maxAuditHTTPBodyBytes+100) + for index := range large { + large[index] = 'a' + } + bounded := auditHTTPBody("/trpc.ocmanager.repository.RepositoryService/CreateRepository", string(large)) + if len(bounded) != maxAuditHTTPBodyBytes { + t.Fatalf("bounded audit body length = %d", len(bounded)) + } +} diff --git a/manager/backend/services/footstone/domain/entity/request/repository.go b/manager/backend/services/footstone/domain/entity/request/repository.go new file mode 100644 index 0000000000000000000000000000000000000000..d6f82a998996e873624e4acef4daf18c13061f2e --- /dev/null +++ b/manager/backend/services/footstone/domain/entity/request/repository.go @@ -0,0 +1,90 @@ +package request + +import "time" + +const ( + RepositoryTypeBaseline = "BASELINE" + RepositoryTypeUpdate = "UPDATE" + RepositoryTypePrivate = "PRIVATE" + + RepositoryArchitectureX8664 = "x86_64" + RepositoryArchitectureARM64 = "aarch64" + RepositoryArchitectureNoarch = "noarch" + + RepositoryGPGDisabled = "DISABLED" + RepositoryGPGAudit = "AUDIT" + RepositoryGPGEnforce = "ENFORCE" + // RepositoryGPGVerifyPackage is retained for repositories created before + // the AUDIT/ENFORCE policy split. It has the same strict behavior as ENFORCE. + RepositoryGPGVerifyPackage = "VERIFY_PACKAGE" + + RepositoryPackageSignatureUnchecked = "UNCHECKED" + RepositoryPackageSignatureValid = "VALID" + RepositoryPackageSignatureUnsigned = "UNSIGNED" + RepositoryPackageSignatureUnauthorized = "UNAUTHORIZED" + RepositoryPackageSignatureBad = "BAD_SIGNATURE" + + RepositorySyncModeMirror = "MIRROR" + RepositorySyncModeAppend = "APPEND" + + RepositoryPurposeNodeRPM = "NODE_RPM" + + RepositoryStatusEmpty = "EMPTY" + RepositoryStatusDraftReady = "DRAFT_READY" + RepositoryStatusReady = "READY" +) + +func RepositoryGPGPolicyEnabled(value string) bool { + return value == RepositoryGPGAudit || RepositoryGPGPolicyEnforced(value) +} + +func RepositoryGPGPolicyEnforced(value string) bool { + return value == RepositoryGPGEnforce || value == RepositoryGPGVerifyPackage +} + +// SoftwareRepositorySpec is the management-plane definition of a repository. +type SoftwareRepositorySpec struct { + ProjectID int64 + SystemProduct string + ProductVersion string + Name string + Description string + Architecture string + Type string + UpstreamURL string + AllowUpload bool + IncludePackages []string + ExcludePackages []string + GPGPolicy string + Enabled bool + PrimaryStorageNodeID string + SyncMode string + BandwidthLimitBytes int64 + MaxParallelDownloads int + MetadataSigningKeyRef string +} + +type ListSoftwareRepositoriesRequest struct { + ProjectID int64 + SystemProduct string + ProductVersion string + Architecture string + Type string + Keyword string + PageRequest PageRequest +} + +type BatchApplySoftwareRepositoryItem struct { + RepositoryID string + Spec SoftwareRepositorySpec +} + +type RepositoryGPGKey struct { + ID string + RepositoryID string + KeyID string + Fingerprint string + PublicKeyPath string + PublicKeyURL string + CreatedAt time.Time +} diff --git a/manager/backend/services/footstone/domain/entity/request/repository_content.go b/manager/backend/services/footstone/domain/entity/request/repository_content.go new file mode 100644 index 0000000000000000000000000000000000000000..8dec437feedf8516ba4ef23b89aa9f5e13cc9326 --- /dev/null +++ b/manager/backend/services/footstone/domain/entity/request/repository_content.go @@ -0,0 +1,180 @@ +package request + +const ( + RepositoryUploadKindRPM = "RPM" + RepositoryUploadKindTarGZ = "TAR_GZ" + + RepositoryUploadStatusReceiving = "RECEIVING" + RepositoryUploadStatusPaused = "PAUSED" + RepositoryUploadStatusReady = "READY" + RepositoryUploadStatusProcessing = "PROCESSING" + RepositoryUploadStatusCompleted = "COMPLETED" + RepositoryUploadStatusFailed = "FAILED" + RepositoryUploadStatusAborted = "ABORTED" + + RepositoryRevisionKindUpload = "UPLOAD" + RepositoryRevisionKindSync = "SYNC" + RepositoryRevisionKindDeletePackage = "DELETE_PACKAGE" + RepositoryRevisionStatusReady = "READY" + RepositoryRevisionStatusPublished = "PUBLISHED" + RepositoryRevisionStatusFailed = "FAILED" + + RepositoryPackageSourceUpload = "UPLOAD" + RepositoryPackageSourceSync = "SYNC" + + RepositoryQuarantineInvalidRPM = "INVALID_RPM" + RepositoryQuarantineDamagedRPM = "DAMAGED_RPM" + RepositoryQuarantineBadSignature = "BAD_SIGNATURE" + RepositoryQuarantineUnsignedRPM = "UNSIGNED_RPM" + RepositoryQuarantineUnauthorizedKey = "UNAUTHORIZED_GPG_KEY" + RepositoryQuarantineWrongArch = "WRONG_ARCHITECTURE" + RepositoryQuarantineNEVRAConflict = "NEVRA_CONFLICT" + RepositoryQuarantineDangerousArchive = "DANGEROUS_ARCHIVE" +) + +type BeginRepositoryUploadRequest struct { + RepositoryID string + FileName string + TotalSize int64 + ExpectedSHA256 string + ClientFingerprint string + ChunkSize int +} + +type AppendRepositoryUploadChunkRequest struct { + UploadID string + Offset int64 + Data []byte +} + +type PutRepositoryUploadChunkRequest struct { + UploadID string + ChunkIndex int + ChunkSHA256 string + Data []byte +} + +type ListRepositoryPackagesRequest struct { + RepositoryID string + RevisionID string + Name string + Architecture string + PageRequest PageRequest +} + +type ListRepositoryQuarantineRequest struct { + RepositoryID string + TaskID string + ReasonCode string + PageRequest PageRequest +} + +type DeleteRepositoryPackageRequest struct { + RepositoryID string + NEVRA string +} + +const RepositoryOperationPackageDelete = "PACKAGE_DELETE" + +type PreviewRepositoryPackageDeletionRequest struct { + RepositoryID string + NEVRAs []string +} + +type ConfirmRepositoryPackageDeletionRequest struct { + PreviewID string + PreviewDigest string +} + +type RepositoryDependencyImpact struct { + PackageNEVRA string `json:"package_nevra"` + Requires string `json:"requires"` + Depth int `json:"depth"` + Direct bool `json:"direct"` + Reason string `json:"reason"` +} + +type RepositoryPackageDeletionPreviewPayload struct { + PreviewID string `json:"preview_id"` + RepositoryID string `json:"repository_id"` + RevisionID string `json:"revision_id"` + RepositorySet []RepositoryPreviewSource `json:"repository_set"` + NEVRAs []string `json:"nevras"` + DependencyImpact []RepositoryDependencyImpact `json:"dependency_impact"` + Command []string `json:"command"` + RawOutput string `json:"raw_output"` +} + +type RepositoryPreviewSource struct { + RepositoryID string `json:"repository_id"` + RevisionID string `json:"revision_id"` + RepoID string `json:"repo_id"` + BaseURL string `json:"baseurl"` +} + +// GeneratedRepositoryConfig is the management-plane representation of one +// deterministic DNF/YUM .repo document for the active immutable revision. +type GeneratedRepositoryConfig struct { + RepositoryID string + ActiveRevisionID string + RepoID string + FileName string + ContentType string + Content string + BaseURL string + RevisionURL string + ImmutableRevision bool + Enabled bool + GPGCheck bool + RepoGPGCheck bool + GPGKeyURLs []string +} + +// RepositorySyncSnapshot freezes mutable repository settings when a sync task +// is created so retries cannot silently switch upstream or filtering policy. +type RepositorySyncSnapshot struct { + UpstreamURL string `json:"upstream_url"` + Architecture string `json:"architecture"` + IncludePackages []string `json:"include_packages"` + ExcludePackages []string `json:"exclude_packages"` + SyncMode string `json:"sync_mode"` + BandwidthLimitBytes int64 `json:"bandwidth_limit_bytes"` + MaxParallelDownloads int `json:"max_parallel_downloads"` +} + +// RepositoryUploadSnapshot freezes the node-side object identity before the task is enqueued. +// The payload contains metadata and a relative object reference, never RPM bytes or credentials. +type RepositoryUploadSnapshot struct { + UploadID string `json:"upload_id"` + FileName string `json:"file_name"` + Kind string `json:"kind"` + RelativePath string `json:"relative_path"` + SizeBytes int64 `json:"size_bytes"` + SHA256 string `json:"sha256"` +} + +// RepositoryMigrationSnapshot freezes one active immutable revision and its target node. +// Workers must never resolve a mutable current/latest URL during migration. +type RepositoryMigrationSnapshot struct { + SourceStorageNodeID string `json:"source_storage_node_id,omitempty"` + TargetStorageNodeID string `json:"target_storage_node_id"` + TargetPublishBaseURL string `json:"target_publish_base_url"` + SourceRevisionURL string `json:"source_revision_url"` + RevisionID string `json:"revision_id"` + RevisionKind string `json:"revision_kind"` + ManifestSHA256 string `json:"manifest_sha256"` + RepomdSHA256 string `json:"repomd_sha256"` + SnapshotManifestSHA256 string `json:"snapshot_manifest_sha256"` + PackageCount int `json:"package_count"` + TotalBytes int64 `json:"total_bytes"` + GPGKeys []RepositoryMigrationGPGKey `json:"gpg_keys,omitempty"` +} + +// RepositoryMigrationGPGKey freezes one public-key reference from the source +// repository node. Private key material is never stored or transferred. +type RepositoryMigrationGPGKey struct { + KeyID string `json:"key_id"` + Fingerprint string `json:"fingerprint"` + PublicKeyPath string `json:"public_key_path"` + SourcePublicURL string `json:"source_public_url"` +} diff --git a/manager/backend/services/footstone/domain/entity/request/repository_distribution.go b/manager/backend/services/footstone/domain/entity/request/repository_distribution.go new file mode 100644 index 0000000000000000000000000000000000000000..54d29205c0668630085716571542e6b4cc1cdb9a --- /dev/null +++ b/manager/backend/services/footstone/domain/entity/request/repository_distribution.go @@ -0,0 +1,54 @@ +package request + +const ( + RepositoryDistributionStatusPending = "PENDING" + RepositoryDistributionStatusRunning = "RUNNING" + RepositoryDistributionStatusSuccess = "SUCCESS" + RepositoryDistributionStatusPartialSuccess = "PARTIAL_SUCCESS" + RepositoryDistributionStatusFailed = "FAILED" + RepositoryDistributionStatusCanceled = "CANCELED" + + RepositoryDistributionTargetPending = "PENDING" + RepositoryDistributionTargetRunning = "RUNNING" + RepositoryDistributionTargetSuccess = "SUCCESS" + RepositoryDistributionTargetFailed = "FAILED" + RepositoryDistributionTargetTimeout = "TIMEOUT" + RepositoryDistributionTargetCanceled = "CANCELED" + RepositoryDistributionTargetDispatching = "DISPATCHING" +) + +type CreateRepositoryDistributionRequest struct { + RepositoryID string + RevisionID string + TargetIPs []string + TimeoutSeconds int +} + +type ExecuteRepositoryDistributionTaskRequest struct { + TaskID string + StorageNodeID string + WorkerID string + FencingToken uint64 +} + +type ListRepositoryDistributionsRequest struct { + RepositoryID string + PageRequest PageRequest +} + +type RepositoryDistributionPayload struct { + DistributionID string `json:"distribution_id"` + TargetIPs []string `json:"target_ips"` + RevisionURL string `json:"revision_url"` + ManifestSHA256 string `json:"manifest_sha256"` + RepomdSHA256 string `json:"repomd_sha256"` + TimeoutSeconds int `json:"timeout_seconds"` +} + +type RepositoryDistributionNode struct { + AgentID uint64 + IP string + AssetStatus string + Connected bool + LastHeartbeatAt int64 +} diff --git a/manager/backend/services/footstone/domain/entity/request/repository_inventory.go b/manager/backend/services/footstone/domain/entity/request/repository_inventory.go new file mode 100644 index 0000000000000000000000000000000000000000..137aa93c43ef8b91e7cb43601ce1d8676db8bee3 --- /dev/null +++ b/manager/backend/services/footstone/domain/entity/request/repository_inventory.go @@ -0,0 +1,60 @@ +package request + +import "time" + +const ( + RepositoryInventorySchemaV1 = "ocmanager.repository.inventory/v1" + RepositoryInventorySchemaV2 = "ocmanager.repository.inventory/v2" + + RepositoryInventoryConflictFail = "FAIL" + RepositoryInventoryConflictSkip = "SKIP" + RepositoryInventoryConflictRename = "RENAME" + + RepositoryInventoryActionCreate = "CREATE" + RepositoryInventoryActionSkip = "SKIP" + RepositoryInventoryActionError = "ERROR" + + RepositoryOperationInventoryImport = "REPOSITORY_IMPORT" + RepositoryInventoryImportSuccess = "SUCCESS" +) + +type PreviewRepositoryInventoryImportRequest struct { + ProjectID int64 + Content string + ConflictStrategy string + DefaultStorageNodeID string + StorageNodeMappings map[string]string + SigningKeyMappings map[string]string +} + +type ConfirmRepositoryInventoryImportRequest struct { + PreviewID string + PreviewDigest string +} + +type RepositoryInventoryImportItem struct { + SourceRepositoryID string + SourceName string + Action string + Message string + Spec SoftwareRepositorySpec +} + +type RepositoryInventoryImportPreviewPayload struct { + SchemaVersion string + ProjectID int64 + ResourceVersion string + Items []RepositoryInventoryImportItem + Warnings []string +} + +type RepositoryInventoryImportPreview struct { + ID string + SchemaVersion string + PreviewDigest string + ResourceVersion string + ExpiresAt time.Time + Status string + Items []RepositoryInventoryImportItem + Warnings []string +} diff --git a/manager/backend/services/footstone/domain/entity/request/repository_node.go b/manager/backend/services/footstone/domain/entity/request/repository_node.go new file mode 100644 index 0000000000000000000000000000000000000000..d06b15475e94c391c9074db49037bade1d2c86a3 --- /dev/null +++ b/manager/backend/services/footstone/domain/entity/request/repository_node.go @@ -0,0 +1,282 @@ +package request + +import ( + "io" + "time" +) + +const ( + RepositoryStorageNodeStatusUnknown = "UNKNOWN" + RepositoryStorageNodeStatusOnline = "ONLINE" + RepositoryStorageNodeStatusOffline = "OFFLINE" + RepositoryStorageNodeStatusDisabled = "DISABLED" + RepositoryStorageNodeStatusPending = "PENDING" + RepositoryStorageNodeStatusInstalling = "INSTALLING" + RepositoryStorageNodeStatusInstallFailed = "INSTALL_FAILED" + + RepositoryNodeEnrollmentPending = "PENDING" + RepositoryNodeEnrollmentInstalling = "INSTALLING" + RepositoryNodeEnrollmentAuthorized = "AUTHORIZED" + RepositoryNodeEnrollmentRegistering = "REGISTERING" + RepositoryNodeEnrollmentRegistered = "REGISTERED" + RepositoryNodeEnrollmentBootstrapped = "BOOTSTRAPPED" + RepositoryNodeEnrollmentOnline = "ONLINE" + RepositoryNodeEnrollmentFailed = "FAILED" + RepositoryNodeEnrollmentExpired = "EXPIRED" + + RepositoryNodeCertificateActive = "ACTIVE" + RepositoryNodeCertificateSuperseded = "SUPERSEDED" + RepositoryNodeCertificateRevoked = "REVOKED" + RepositoryNodeCertificateExpired = "EXPIRED" + + RepositoryNodeArtifactAvailable = "AVAILABLE" + RepositoryNodeArtifactPublished = "PUBLISHED" + RepositoryNodeArtifactDisabled = "DISABLED" + RepositoryNodeArtifactLegacyLocal = "LEGACY_LOCAL" + RepositoryNodeArtifactRepository = "REPOSITORY" + + RepositoryNodeInstallSourceEnabled = "ENABLED" + RepositoryNodeInstallSourceDisabled = "DISABLED" + RepositoryNodeAllowlistEnabled = "ENABLED" + RepositoryNodeAllowlistDisabled = "DISABLED" + + RepositoryNodeIssuancePending = "PENDING" + RepositoryNodeIssuanceAuthorized = "AUTHORIZED" + RepositoryNodeIssuanceIssuing = "ISSUING" + RepositoryNodeIssuanceIssued = "ISSUED" + RepositoryNodeIssuanceFailed = "FAILED" + + RepositoryPreflightStatusPass = "PASS" + RepositoryPreflightStatusFail = "FAIL" + + RepositoryStatusMigrationRequired = "MIGRATION_REQUIRED" +) + +type RepositoryStorageNodeSpec struct { + ProjectID int64 + Name string + Description string + ControlBaseURL string + PublishBaseURL string + TLSServerName string + CertificateFingerprint string + Enabled bool + Labels map[string]string +} + +type RepositoryNodeEnrollmentSpec struct { + Node RepositoryStorageNodeSpec + TMSAgentID uint64 + ExpectedArchitecture string + ObservedHostname string + ObservedIP string + ArtifactID string + InstallationMode string +} + +type RepositoryNodeBootstrapRequest struct { + EnrollmentID string + TaskID string + TMSAgentID uint64 + AuthorizationID string + Architecture string + Token string + CSRPEM []byte +} + +type RepositoryNodeRenewCertificateRequest struct { + NodeID string + CSRPEM []byte +} + +type RepositoryNodeCertificateBundle struct { + NodeID string + CertificatePEM []byte + CACertificatePEM []byte + SerialNumber string + Fingerprint string + NotBefore time.Time + NotAfter time.Time +} + +type RepositoryIssuedCertificate struct { + PEM []byte + SerialNumber string + Fingerprint string + NotBefore time.Time + NotAfter time.Time +} + +type RepositoryNodeLifecyclePolicy struct { + EnrollmentTTL time.Duration + CertificateTTL time.Duration + RenewBefore time.Duration + CertificateGrace time.Duration +} + +type RepositoryNodeArtifactSpec struct { + ProjectID int64 + SourceKind string + SourceRepositoryID string + RevisionID string + PackageID string + Name string + Epoch uint64 + Version string + Release string + Architecture string + NEVRA string + FileName string + SizeBytes int64 + SHA256 string + BuildCommit string + RelativePath string + ImmutableURL string + Status string +} + +type RepositoryNodeInstallSourceSpec struct { + ProjectID int64 + RepositoryID string + Architecture string + Status string +} + +type RepositoryNodeRegistrationAllowlistSpec struct { + ProjectID int64 + TMSAgentID uint64 + ExpectedHostname string + ExpectedIP string + ExpectedArchitecture string + Status string + ExpiresAt *time.Time +} + +type RepositoryNodeArtifactDownload struct { + FileName string + ContentType string + SizeBytes int64 + SHA256 string + Content io.ReadCloser +} + +type ListRepositoryStorageNodesRequest struct { + ProjectID int64 + Status string + Keyword string + PageRequest PageRequest +} + +// RepositoryStorageNodeStatistics is computed from the full filtered node set, +// never from the current page. +type RepositoryStorageNodeStatistics struct { + TotalNodes int64 + OnlineNodes int64 + TotalCapacityBytes int64 + UsedCapacityBytes int64 + CriticalNodes int64 +} + +type RepositoryStorageNodeHeartbeat struct { + NodeID string + Hostname string + IPAddress string + Architecture string + OSName string + OSVersion string + AgentVersion string + FilesystemType string + CapacityBytes int64 + FreeBytes int64 + FreeInodes int64 + ToolVersions map[string]string + ObservedAt time.Time + ErrorCode string + ErrorMessage string + RepositoryUsages []RepositoryNodeRepositoryUsage + RepositoryUsageSnapshot bool + MetadataSigningKeys []RepositoryMetadataSigningKey + MetadataKeySnapshot bool +} + +type RepositoryMetadataSigningKey struct { + KeyRef string + Fingerprint string + PublicKeyName string + PublicKeyURL string + Status string + KeyCreatedAt *time.Time + ObservedAt time.Time +} + +type RepositoryNodeRepositoryUsage struct { + RepositoryID string + LogicalBytes int64 + PhysicalBytes int64 + PublishStatus string + ObservedAt time.Time +} + +type RepositoryPreflightResult struct { + ID string + StorageNodeID string + RepositoryID string + TaskID string + Status string + FilesystemType string + CapacityBytes int64 + FreeBytes int64 + RequiredFreeBytes int64 + FreeInodes int64 + RequiredInodes int64 + EstimatedPackageCount int64 + EstimatedImportBytes int64 + ToolVersions map[string]string + ErrorCode string + ErrorMessage string + ExpiresAt time.Time +} + +// RepositoryPreflightPolicy contains the conservative capacity limits used by +// the control plane before a mutating task is allowed onto a repository node. +type RepositoryPreflightPolicy struct { + MaxExpandedBytes int64 + MaxSyncBytes int64 + MaxSyncPackages int64 + FilteredSyncBytes int64 + FilteredSyncPackages int64 + MaxArchiveFiles int64 + MetadataReserve int64 + MinimumFreeBytes int64 + SafetyPercent int64 + HardUsagePercent int64 + TTL time.Duration +} + +type ListRepositoryPreflightRecordsRequest struct { + StorageNodeID string + RepositoryID string + PageRequest PageRequest +} + +type RepositoryTaskLogMetadata struct { + TaskID string + RelativePath string + SHA256 string + CompressedBytes int64 + UncompressedBytes int64 + LineCount int64 + Summary string + ErrorFingerprint string + CompletionStatus string + ExpiresAt time.Time + CreatedAt time.Time +} + +type RepositoryTaskLogDownload struct { + FileName string + ContentType string + SHA256 string + SizeBytes int64 + Content io.ReadCloser +} diff --git a/manager/backend/services/footstone/domain/entity/request/repository_ocai.go b/manager/backend/services/footstone/domain/entity/request/repository_ocai.go new file mode 100644 index 0000000000000000000000000000000000000000..084cc27cf8c65614477ae0245297453ab8a29432 --- /dev/null +++ b/manager/backend/services/footstone/domain/entity/request/repository_ocai.go @@ -0,0 +1,30 @@ +package request + +import "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + +type RepositoryOCAIContext struct { + TaskID string `json:"task_id"` + RepositoryID string `json:"repository_id"` + RepositoryName string `json:"repository_name"` + SystemProduct string `json:"system_product"` + ProductVersion string `json:"product_version"` + Architecture string `json:"architecture"` + TaskType string `json:"task_type"` + TaskStatus string `json:"task_status"` + StorageNodeID string `json:"storage_node_id,omitempty"` + RevisionID string `json:"revision_id,omitempty"` + CurrentStep string `json:"current_step,omitempty"` + ErrorCode string `json:"error_code,omitempty"` + ErrorMessage string `json:"error_message,omitempty"` + Diagnosis *po.RepositoryDiagnosis `json:"diagnosis,omitempty"` + LogSummary string `json:"log_summary,omitempty"` + ContextJSON string `json:"-"` + ContextSHA256 string `json:"-"` + Truncated bool `json:"-"` +} + +type RepositoryOCAIAnalysisResult struct { + Analysis *po.RepositoryAIAnalysis + Context *RepositoryOCAIContext + Instruction string +} diff --git a/manager/backend/services/footstone/domain/entity/request/repository_package_install.go b/manager/backend/services/footstone/domain/entity/request/repository_package_install.go new file mode 100644 index 0000000000000000000000000000000000000000..6cd4d97c9a12543cae39028fca3c230f7cdcb764 --- /dev/null +++ b/manager/backend/services/footstone/domain/entity/request/repository_package_install.go @@ -0,0 +1,67 @@ +package request + +import "time" + +const ( + RepositoryOperationPackageInstall = "PACKAGE_INSTALL" + + RepositoryOperationPreviewReady = "READY" + RepositoryOperationPreviewProcessing = "PROCESSING" + RepositoryOperationPreviewFailed = "FAILED" + RepositoryOperationPreviewConfirmed = "CONFIRMED" + RepositoryOperationPreviewExpired = "EXPIRED" + + RepositoryPackageInstallPending = "PENDING" + RepositoryPackageInstallRunning = "RUNNING" + RepositoryPackageInstallSuccess = "SUCCESS" + RepositoryPackageInstallPartialSuccess = "PARTIAL_SUCCESS" + RepositoryPackageInstallFailed = "FAILED" + RepositoryPackageInstallCanceled = "CANCELED" +) + +type PreviewRepositoryPackageInstallRequest struct { + RepositoryID string + RevisionID string + PackageNames []string + TargetAgentIDs []uint64 + TimeoutSeconds int +} + +type ConfirmRepositoryPackageInstallRequest struct { + PreviewID string + PreviewDigest string +} + +type RepositoryPackageInstallTarget struct { + AgentID uint64 `json:"agent_id"` + IP string `json:"ip"` + Status string `json:"status"` + ExitCode int32 `json:"exit_code"` + ErrorCode string `json:"error_code,omitempty"` + ErrorMessage string `json:"error_message,omitempty"` + RawOutput string `json:"raw_output,omitempty"` + OutputTruncated bool `json:"output_truncated,omitempty"` + SelectedPackages []string `json:"selected_packages,omitempty"` + DependencyPackages []string `json:"dependency_packages,omitempty"` + UpgradePackages []string `json:"upgrade_packages,omitempty"` + ReplacementPackages []string `json:"replacement_packages,omitempty"` + DownloadBytes int64 `json:"download_bytes,omitempty"` + DiskChangeBytes int64 `json:"disk_change_bytes,omitempty"` + StartedAt time.Time `json:"started_at,omitempty"` + FinishedAt time.Time `json:"finished_at,omitempty"` +} + +type RepositoryPackageInstallPreviewPayload struct { + RepositoryID string `json:"repository_id"` + RevisionID string `json:"revision_id"` + RepoID string `json:"repo_id"` + RepoConfig string `json:"repo_config"` + PackageNames []string `json:"package_names"` + Targets []RepositoryPackageInstallTarget `json:"targets"` + Timeout int `json:"timeout_seconds"` +} + +type RepositoryPackageInstallResult struct { + InstallID string + TaskID string +} diff --git a/manager/backend/services/footstone/domain/entity/request/repository_report.go b/manager/backend/services/footstone/domain/entity/request/repository_report.go new file mode 100644 index 0000000000000000000000000000000000000000..b443b04e76dc5be4e3bc12355ef445439bc062b2 --- /dev/null +++ b/manager/backend/services/footstone/domain/entity/request/repository_report.go @@ -0,0 +1,27 @@ +package request + +import ( + "crypto/sha256" + "encoding/json" + "fmt" +) + +type RepositoryExport struct { + FileName string + ContentType string + SHA256 string + Content string +} + +func NewRepositoryJSONExport(fileName string, report interface{}) (*RepositoryExport, error) { + content, err := json.MarshalIndent(report, "", " ") + if err != nil { + return nil, fmt.Errorf("encode repository report: %w", err) + } + content = append(content, '\n') + digest := sha256.Sum256(content) + return &RepositoryExport{ + FileName: fileName, ContentType: "application/json", + SHA256: fmt.Sprintf("%x", digest), Content: string(content), + }, nil +} diff --git a/manager/backend/services/footstone/domain/entity/request/repository_schedule.go b/manager/backend/services/footstone/domain/entity/request/repository_schedule.go new file mode 100644 index 0000000000000000000000000000000000000000..365bfea7de60f2813f8908138371e57ff624ab0e --- /dev/null +++ b/manager/backend/services/footstone/domain/entity/request/repository_schedule.go @@ -0,0 +1,26 @@ +package request + +const ( + RepositoryScheduleCadenceDaily = "DAILY" + RepositoryScheduleCadenceWeekly = "WEEKLY" + + RepositoryScheduleStatusEnabled = "ENABLED" + RepositoryScheduleStatusPaused = "PAUSED" + RepositoryScheduleStatusAutoPaused = "AUTO_PAUSED" + + RepositoryScheduleRunPending = "PENDING" + RepositoryScheduleRunTaskCreated = "TASK_CREATED" + RepositoryScheduleRunSkippedAlreadyRunning = "SKIPPED_ALREADY_RUNNING" + RepositoryScheduleRunSuccess = "SUCCESS" + RepositoryScheduleRunFailed = "FAILED" +) + +type RepositorySyncScheduleSpec struct { + RepositoryID string + Cadence string + Timezone string + DayOfWeek int + Hour int + Minute int + FailureThreshold int +} diff --git a/manager/backend/services/footstone/domain/entity/request/repository_task.go b/manager/backend/services/footstone/domain/entity/request/repository_task.go new file mode 100644 index 0000000000000000000000000000000000000000..c49009f0589a459757d1275716612fe58252752b --- /dev/null +++ b/manager/backend/services/footstone/domain/entity/request/repository_task.go @@ -0,0 +1,147 @@ +package request + +import "time" + +const ( + RepositoryTaskTypeSync = "SYNC" + RepositoryTaskTypeUpload = "UPLOAD" + RepositoryTaskTypeDeletePackage = "DELETE_PACKAGE" + RepositoryTaskTypeRebuildMetadata = "REBUILD_METADATA" + RepositoryTaskTypeDeleteRepository = "DELETE_REPOSITORY" + RepositoryTaskTypePublish = "PUBLISH" + RepositoryTaskTypeValidate = "VALIDATE" + RepositoryTaskTypeDistribute = "DISTRIBUTE" + RepositoryTaskTypeMigrateRepository = "MIGRATE_REPOSITORY" + RepositoryTaskTypeNodeInstall = "NODE_INSTALL" + RepositoryTaskTypePackageInstall = "PACKAGE_INSTALL" + RepositoryTaskTypeInventoryImport = "INVENTORY_IMPORT" + + RepositoryTaskScopeRepository = "REPOSITORY" + RepositoryTaskScopeNode = "NODE" + RepositoryTaskScopeAgent = "AGENT" + RepositoryTaskScopeProject = "PROJECT" + + RepositoryTaskExecutorWorker = "REPOSITORY_WORKER" + RepositoryTaskExecutorTMS = "TMS" + RepositoryTaskExecutorManual = "MANUAL" + + RepositoryTaskStatusPending = "PENDING" + RepositoryTaskStatusRunning = "RUNNING" + RepositoryTaskStatusCancelRequested = "CANCEL_REQUESTED" + RepositoryTaskStatusCanceled = "CANCELED" + RepositoryTaskStatusSuccess = "SUCCESS" + RepositoryTaskStatusFailed = "FAILED" + RepositoryTaskStatusPartialSuccess = "PARTIAL_SUCCESS" + + RepositoryTaskCommitStateNone = "NONE" + RepositoryTaskCommitStatePrepared = "PREPARED" + RepositoryTaskCommitStateConfirmed = "CONFIRMED" + + RepositoryTaskTriggerManual = "MANUAL" + RepositoryTaskTriggerScheduled = "SCHEDULED" + RepositoryTaskTriggerRetry = "RETRY" + RepositoryTaskTriggerSystem = "SYSTEM" +) + +type EnqueueRepositoryTaskRequest struct { + RepositoryID string + RevisionID string + PreflightID string + Type string + PayloadJSON string + MaxAttempts int + Trigger string + IdempotencyKey string + BandwidthLimitBytes int64 + MaxParallelDownloads int +} + +type ListRepositoryTasksRequest struct { + ProjectID int64 + RepositoryID string + StorageNodeID string + Type string + Status string + Trigger string + CreatedAtFrom *time.Time + CreatedAtTo *time.Time + PageRequest PageRequest +} + +// RepositoryTaskStatistics is the four-state operational summary. Transitional +// cancellation states are folded into RUNNING/FAILED and PARTIAL_SUCCESS is +// folded into SUCCESS so every persisted task contributes to one bucket. +type RepositoryTaskStatistics struct { + PendingCount int64 + RunningCount int64 + FailedCount int64 + SuccessCount int64 +} + +type HeartbeatRepositoryTaskRequest struct { + TaskID string + StorageNodeID string + WorkerID string + FencingToken uint64 + LeaseSeconds int + ProgressPercent int32 + CurrentStep string +} + +type CompleteRepositoryTaskRequest struct { + TaskID string + StorageNodeID string + WorkerID string + FencingToken uint64 + Status string + ErrorCode string + ErrorMessage string + ResultObjectID string + ResultObjectSHA256 string + QuarantineCount int64 +} + +type AttachRepositoryTaskRevisionRequest struct { + TaskID string + WorkerID string + FencingToken uint64 + RevisionID string +} + +type ExecuteRepositoryMutationRequest struct { + TaskID string + WorkerID string + FencingToken uint64 + RevisionID string +} + +type PrepareRepositoryTaskCommitRequest struct { + TaskID string + StorageNodeID string + WorkerID string + FencingToken uint64 + RevisionID string + ManifestSHA256 string + ResultObjectID string + ResultObjectSHA256 string + PackageCount int64 + TotalBytes int64 + CompletionStatus string + ErrorCode string + ErrorMessage string +} + +type ConfirmRepositoryTaskCommitRequest struct { + TaskID string + StorageNodeID string + WorkerID string + FencingToken uint64 + CommitToken string +} + +type RepositoryDiagnosisExport struct { + FileName string + ContentType string + SHA256 string + Content string +} diff --git a/manager/backend/services/footstone/domain/entity/request/repository_upstream.go b/manager/backend/services/footstone/domain/entity/request/repository_upstream.go new file mode 100644 index 0000000000000000000000000000000000000000..d28bbe4a7249daa6905484039c6759b36f9d3ca2 --- /dev/null +++ b/manager/backend/services/footstone/domain/entity/request/repository_upstream.go @@ -0,0 +1,23 @@ +package request + +type RepositoryUpstreamProfileSpec struct { + ProjectID int64 + Name string + Description string + SystemProduct string + Channel string + URLTemplate string + SupportedVersions []string + SupportedArchitectures []string + DefaultIncludePackages []string + DefaultExcludePackages []string + Enabled bool +} + +type ListRepositoryUpstreamProfilesRequest struct { + ProjectID int64 + SystemProduct string + Keyword string + Enabled *bool + PageRequest PageRequest +} diff --git a/manager/backend/services/footstone/domain/repository/instancerepo/ip_filter_test.go b/manager/backend/services/footstone/domain/repository/instancerepo/ip_filter_test.go index 6c3fc70684d3ec7bc9f77f73a2a24f5f39fcf53b..8040022668157411c9e9294a86bd4202eafd9829 100644 --- a/manager/backend/services/footstone/domain/repository/instancerepo/ip_filter_test.go +++ b/manager/backend/services/footstone/domain/repository/instancerepo/ip_filter_test.go @@ -1,7 +1,6 @@ package instancerepo import ( - "fmt" "reflect" "strings" "testing" @@ -64,10 +63,8 @@ func TestInstanceIPFilterUsesGORMBindings(t *testing.T) { } where, args := buildInstanceIPFilter("2001:db8::1", []string{"192.0.2.1", maliciousIP}) - join := fmt.Sprintf("left join lateral ("+ - "select ip from instance_ip_mappings where instance_id = ins.id %s limit 1"+ - ") ii on true", where) - query := db.Table("cluster_instance_mappings ci").Joins(join, args...).Find(&[]struct{}{}) + query := db.Table("instance_ip_mappings").Select("distinct(instance_id)"). + Where(strings.TrimPrefix(where, " and "), args...).Find(&[]struct{}{}) if query.Error != nil { t.Fatalf("build dry-run query: %v", query.Error) } @@ -79,6 +76,9 @@ func TestInstanceIPFilterUsesGORMBindings(t *testing.T) { if !strings.Contains(sql, "ip in (?,?) and ip = ?") { t.Fatalf("generated SQL does not use expected placeholders: %q", sql) } + if strings.Contains(strings.ToLower(sql), "lateral") { + t.Fatalf("generated SQL must remain compatible with MariaDB: %q", sql) + } wantVars := []interface{}{"192.0.2.1", maliciousIP, "2001:db8::1"} if !reflect.DeepEqual(query.Statement.Vars, wantVars) { t.Fatalf("generated SQL vars = %#v, want %#v", query.Statement.Vars, wantVars) diff --git a/manager/backend/services/footstone/domain/repository/instancerepo/repo.go b/manager/backend/services/footstone/domain/repository/instancerepo/repo.go index 5ba7df6a7d049e5d128e40de22b03015ee0195f5..fc28b38fa6532782b3f77fcfd5211e0c7e99b19f 100644 --- a/manager/backend/services/footstone/domain/repository/instancerepo/repo.go +++ b/manager/backend/services/footstone/domain/repository/instancerepo/repo.go @@ -634,20 +634,15 @@ func (repo *instanceRepo) ListClusterInstanceMappings(req request.ListInstancesR resp = request.ListInstancesResponse{ Instances: make([]request.InstanceInfo, 0), } - sub string subWhere string subArgs []interface{} ids []int64 ) defer maker.Close(&err) subWhere, subArgs = buildInstanceIPFilter(req.IP, req.IPList) - sub = fmt.Sprintf("left join lateral ("+ - "select ip from instance_ip_mappings where instance_id = ins.id %s limit 1"+ - ") ii on true", subWhere) tx = tx.Table("cluster_instance_mappings ci"). Select("ins.*,ci.project_id,ci.cluster_id,ci.datasource,ci.id as mapping_id"). - Joins("left join instances ins on ins.id=ci.instance_id"). - Joins(sub, subArgs...) + Joins("left join instances ins on ins.id=ci.instance_id") tx2 := repo.dbclient.DB() if req.IP != "" || len(req.IPList) != 0 { diff --git a/manager/backend/services/footstone/domain/repository/po/repository_acceptance_closure.go b/manager/backend/services/footstone/domain/repository/po/repository_acceptance_closure.go new file mode 100644 index 0000000000000000000000000000000000000000..602d7170eaae45005c9c7e7b49846f7de4884f5a --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/po/repository_acceptance_closure.go @@ -0,0 +1,58 @@ +package po + +import "time" + +// RepositoryNodeInstallSource identifies the project-scoped Node RPM source +// for one canonical architecture. +type RepositoryNodeInstallSource struct { + ID string `json:"id" gorm:"column:id;type:char(36);primaryKey"` + ProjectID int64 `json:"project_id" gorm:"column:project_id;type:bigint;not null;uniqueIndex:uniq_repository_node_install_source_arch,priority:1"` + RepositoryID string `json:"repository_id" gorm:"column:repository_id;type:char(36);not null;index:idx_repository_node_install_sources_repository"` + Architecture string `json:"architecture" gorm:"column:architecture;type:varchar(32);not null;uniqueIndex:uniq_repository_node_install_source_arch,priority:2"` + Status string `json:"status" gorm:"column:status;type:varchar(32);not null;default:'ENABLED'"` + CreatedBy string `json:"created_by" gorm:"column:created_by;type:varchar(256);not null"` + UpdatedBy string `json:"updated_by" gorm:"column:updated_by;type:varchar(256);not null"` + CreatedAt time.Time `json:"created_at" gorm:"<-:create;autoCreateTime;column:created_at;type:datetime(3);not null"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;column:updated_at;type:datetime(3);not null"` +} + +func (RepositoryNodeInstallSource) TableName() string { return "repository_node_install_sources" } + +// RepositoryNodeRegistrationAllowlist is the strong TMS Agent authorization +// checked both when enrollment is created and when bootstrap is authorized. +type RepositoryNodeRegistrationAllowlist struct { + ID string `json:"id" gorm:"column:id;type:char(36);primaryKey"` + ProjectID int64 `json:"project_id" gorm:"column:project_id;type:bigint;not null;uniqueIndex:uniq_repository_node_allowlist_agent,priority:1;index:idx_repository_node_allowlists_status,priority:1"` + TMSAgentID uint64 `json:"tms_agent_id" gorm:"column:tms_agent_id;type:bigint unsigned;not null;uniqueIndex:uniq_repository_node_allowlist_agent,priority:2"` + ExpectedHostname string `json:"expected_hostname" gorm:"column:expected_hostname;type:varchar(255);not null;default:''"` + ExpectedIP string `json:"expected_ip" gorm:"column:expected_ip;type:varchar(64);not null;default:''"` + ExpectedArchitecture string `json:"expected_architecture" gorm:"column:expected_architecture;type:varchar(32);not null;default:''"` + Status string `json:"status" gorm:"column:status;type:varchar(32);not null;default:'ENABLED';index:idx_repository_node_allowlists_status,priority:2"` + ExpiresAt *time.Time `json:"expires_at" gorm:"column:expires_at;type:datetime(3);index:idx_repository_node_allowlists_status,priority:3"` + CreatedBy string `json:"created_by" gorm:"column:created_by;type:varchar(256);not null"` + UpdatedBy string `json:"updated_by" gorm:"column:updated_by;type:varchar(256);not null"` + CreatedAt time.Time `json:"created_at" gorm:"<-:create;autoCreateTime;column:created_at;type:datetime(3);not null"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;column:updated_at;type:datetime(3);not null"` +} + +func (RepositoryNodeRegistrationAllowlist) TableName() string { + return "repository_node_registration_allowlists" +} + +// RepositoryTaskResultItem stores one package result under a single batch +// task. The task remains the authoritative operation state. +type RepositoryTaskResultItem struct { + ID string `json:"id" gorm:"column:id;type:char(36);primaryKey"` + TaskID string `json:"task_id" gorm:"column:task_id;type:char(36);not null;uniqueIndex:uniq_repository_task_result_item,priority:1;index:idx_repository_task_result_items_status,priority:1"` + ItemKey string `json:"item_key" gorm:"column:item_key;type:varchar(512);not null;uniqueIndex:uniq_repository_task_result_item,priority:2"` + PackageNEVRA string `json:"package_nevra" gorm:"column:package_nevra;type:varchar(512);not null;default:''"` + Status string `json:"status" gorm:"column:status;type:varchar(32);not null;index:idx_repository_task_result_items_status,priority:2"` + ErrorCode string `json:"error_code" gorm:"column:error_code;type:varchar(64);not null;default:''"` + Message string `json:"message" gorm:"column:message;type:varchar(2048);not null;default:''"` + ResultJSON []byte `json:"-" gorm:"column:result_json;type:json"` + StartedAt *time.Time `json:"started_at" gorm:"column:started_at;type:datetime(3)"` + FinishedAt *time.Time `json:"finished_at" gorm:"column:finished_at;type:datetime(3)"` + CreatedAt time.Time `json:"created_at" gorm:"<-:create;autoCreateTime;column:created_at;type:datetime(3);not null"` +} + +func (RepositoryTaskResultItem) TableName() string { return "repository_task_result_items" } diff --git a/manager/backend/services/footstone/domain/repository/po/repository_content.go b/manager/backend/services/footstone/domain/repository/po/repository_content.go new file mode 100644 index 0000000000000000000000000000000000000000..29a76c9b71f49a6117a7e563e1f16d821a61cc89 --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/po/repository_content.go @@ -0,0 +1,131 @@ +package po + +import "time" + +// RepositoryUpload is a resumable control-plane upload session. The file body +// lives in the repository bind mount; the database stores metadata only. +type RepositoryUpload struct { + ID string `json:"id" gorm:"column:id;type:char(36);primaryKey"` + RepositoryID string `json:"repository_id" gorm:"column:repository_id;type:char(36);not null;index:idx_repository_uploads_repository"` + ProjectID int64 `json:"project_id" gorm:"column:project_id;type:bigint;not null;index:idx_repository_uploads_project"` + StorageNodeID string `json:"storage_node_id" gorm:"column:storage_node_id;type:char(36);not null;default:'';index:idx_repository_uploads_storage_node"` + ClientFingerprint string `json:"client_fingerprint" gorm:"column:client_fingerprint;type:char(64);not null;default:''"` + TaskID string `json:"task_id" gorm:"column:task_id;type:char(36);not null;default:'';index:idx_repository_uploads_task"` + FileName string `json:"file_name" gorm:"column:file_name;type:varchar(255);not null"` + Kind string `json:"kind" gorm:"column:upload_kind;type:varchar(16);not null"` + ExpectedSize int64 `json:"expected_size" gorm:"column:expected_size;type:bigint;not null"` + ChunkSize int `json:"chunk_size" gorm:"column:chunk_size;type:int;not null;default:0"` + TotalChunks int `json:"total_chunks" gorm:"column:total_chunks;type:int;not null;default:0"` + UploadedCount int `json:"uploaded_count" gorm:"column:uploaded_count;type:int;not null;default:0"` + ReceivedSize int64 `json:"received_size" gorm:"column:received_size;type:bigint;not null;default:0"` + ExpectedSHA256 string `json:"expected_sha256" gorm:"column:expected_sha256;type:char(64);not null;default:''"` + ActualSHA256 string `json:"actual_sha256" gorm:"column:actual_sha256;type:char(64);not null;default:''"` + RelativePath string `json:"relative_path" gorm:"column:relative_path;type:varchar(512);not null"` + Status string `json:"status" gorm:"column:status;type:varchar(32);not null;index:idx_repository_uploads_status"` + RequestedBy string `json:"requested_by" gorm:"column:requested_by;type:varchar(256);not null"` + ErrorCode string `json:"error_code" gorm:"column:error_code;type:varchar(64);not null;default:''"` + ErrorMessage string `json:"error_message" gorm:"column:error_message;type:varchar(2048);not null;default:''"` + ExpiresAt time.Time `json:"expires_at" gorm:"column:expires_at;type:datetime(3);not null;index:idx_repository_uploads_expires"` + CompletedAt *time.Time `json:"completed_at" gorm:"column:completed_at;type:datetime(3)"` + CreatedAt time.Time `json:"created_at" gorm:"<-:create;autoCreateTime;column:created_at;type:datetime(3);not null"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;column:updated_at;type:datetime(3);not null"` + PausedAt *time.Time `json:"paused_at" gorm:"column:paused_at;type:datetime(3)"` +} + +func (RepositoryUpload) TableName() string { return "repository_uploads" } + +// RepositoryUploadChunk is the authoritative record for one indexed upload +// chunk. Aggregate counters on RepositoryUpload are caches rebuilt from these +// rows and must not be used as a second source of truth. +type RepositoryUploadChunk struct { + UploadID string `json:"upload_id" gorm:"column:upload_id;type:char(36);primaryKey"` + ChunkIndex int `json:"chunk_index" gorm:"column:chunk_index;type:int;primaryKey"` + SizeBytes int `json:"size_bytes" gorm:"column:size_bytes;type:int;not null"` + SHA256 string `json:"sha256" gorm:"column:sha256;type:char(64);not null"` + StoredPath string `json:"stored_path" gorm:"column:stored_path;type:varchar(512);not null"` + CreatedAt time.Time `json:"created_at" gorm:"<-:create;autoCreateTime;column:created_at;type:datetime(3);not null"` +} + +func (RepositoryUploadChunk) TableName() string { return "repository_upload_chunks" } + +// RepositoryRevision is an immutable package snapshot. Phase 3 creates READY +// private revisions; Phase 4 will add metadata and publication state. +type RepositoryRevision struct { + ID string `json:"id" gorm:"column:id;type:char(36);primaryKey"` + RepositoryID string `json:"repository_id" gorm:"column:repository_id;type:char(36);not null;index:idx_repository_revisions_repository,priority:1"` + StorageNodeID string `json:"storage_node_id" gorm:"column:storage_node_id;type:char(36);not null;default:'';index:idx_repository_revisions_storage_node"` + ParentRevisionID string `json:"parent_revision_id" gorm:"column:parent_revision_id;type:char(36);not null;default:''"` + TaskID string `json:"task_id" gorm:"column:task_id;type:char(36);not null;uniqueIndex:uniq_repository_revisions_task"` + Kind string `json:"kind" gorm:"column:revision_kind;type:varchar(32);not null"` + Status string `json:"status" gorm:"column:status;type:varchar(32);not null;index:idx_repository_revisions_repository,priority:2"` + PackageCount int `json:"package_count" gorm:"column:package_count;type:int;not null"` + TotalBytes int64 `json:"total_bytes" gorm:"column:total_bytes;type:bigint;not null"` + ManifestSHA256 string `json:"manifest_sha256" gorm:"column:manifest_sha256;type:char(64);not null"` + RepomdSHA256 string `json:"repomd_sha256" gorm:"column:repomd_sha256;type:char(64);not null;default:''"` + MetadataSigned bool `json:"metadata_signed" gorm:"column:metadata_signed;type:tinyint(1);not null;default:0"` + MetadataSigningKeyRef string `json:"metadata_signing_key_ref" gorm:"column:metadata_signing_key_ref;type:varchar(128);not null;default:''"` + MetadataSigningFingerprint string `json:"metadata_signing_fingerprint" gorm:"column:metadata_signing_fingerprint;type:varchar(64);not null;default:''"` + RepomdSignaturePath string `json:"repomd_signature_path" gorm:"column:repomd_signature_path;type:varchar(512);not null;default:''"` + MetadataPublicKeyURL string `json:"metadata_public_key_url" gorm:"column:metadata_public_key_url;type:varchar(2048);not null;default:''"` + SnapshotManifestSHA256 string `json:"snapshot_manifest_sha256" gorm:"column:snapshot_manifest_sha256;type:char(64);not null;default:''"` + RelativePath string `json:"relative_path" gorm:"column:relative_path;type:varchar(512);not null"` + CreatedBy string `json:"created_by" gorm:"column:created_by;type:varchar(256);not null"` + CreatedAt time.Time `json:"created_at" gorm:"<-:create;autoCreateTime;column:created_at;type:datetime(3);not null;index:idx_repository_revisions_repository,priority:3"` + PublishedAt *time.Time `json:"published_at" gorm:"column:published_at;type:datetime(3)"` +} + +func (RepositoryRevision) TableName() string { return "repository_revisions" } + +type RepositoryPackage struct { + ID string `json:"id" gorm:"column:id;type:char(36);primaryKey"` + RepositoryID string `json:"repository_id" gorm:"column:repository_id;type:char(36);not null;index:idx_repository_packages_repository"` + RevisionID string `json:"revision_id" gorm:"column:revision_id;type:char(36);not null;uniqueIndex:uniq_repository_revision_nevra,priority:1;index:idx_repository_packages_revision"` + Name string `json:"name" gorm:"column:name;type:varchar(255);not null;index:idx_repository_packages_name"` + Epoch string `json:"epoch" gorm:"column:epoch;type:varchar(32);not null;default:'0'"` + Version string `json:"version" gorm:"column:version;type:varchar(255);not null"` + Release string `json:"release" gorm:"column:package_release;type:varchar(255);not null"` + Architecture string `json:"architecture" gorm:"column:architecture;type:varchar(32);not null;index:idx_repository_packages_arch"` + NEVRA string `json:"nevra" gorm:"column:nevra;type:varchar(512);not null;uniqueIndex:uniq_repository_revision_nevra,priority:2"` + FileName string `json:"file_name" gorm:"column:file_name;type:varchar(255);not null"` + RelativePath string `json:"relative_path" gorm:"column:relative_path;type:varchar(512);not null"` + SizeBytes int64 `json:"size_bytes" gorm:"column:size_bytes;type:bigint;not null"` + SHA256 string `json:"sha256" gorm:"column:sha256;type:char(64);not null"` + Source string `json:"source" gorm:"column:package_source;type:varchar(32);not null"` + SourceUploadID string `json:"source_upload_id" gorm:"column:source_upload_id;type:char(36);not null;default:''"` + SignatureStatus string `json:"signature_status" gorm:"column:signature_status;type:varchar(32);not null;default:'UNCHECKED';index:idx_repository_packages_signature"` + SignerKeyID string `json:"signer_key_id" gorm:"column:signer_key_id;type:varchar(64);not null;default:''"` + SignatureMessage string `json:"signature_message" gorm:"column:signature_message;type:varchar(1024);not null;default:''"` + CreatedAt time.Time `json:"created_at" gorm:"<-:create;autoCreateTime;column:created_at;type:datetime(3);not null"` +} + +func (RepositoryPackage) TableName() string { return "repository_packages" } + +type RepositoryQuarantineItem struct { + ID string `json:"id" gorm:"column:id;type:char(36);primaryKey"` + RepositoryID string `json:"repository_id" gorm:"column:repository_id;type:char(36);not null;index:idx_repository_quarantine_repository"` + TaskID string `json:"task_id" gorm:"column:task_id;type:char(36);not null;index:idx_repository_quarantine_task"` + UploadID string `json:"upload_id" gorm:"column:upload_id;type:char(36);not null;default:''"` + OriginalName string `json:"original_name" gorm:"column:original_name;type:varchar(255);not null"` + RelativePath string `json:"relative_path" gorm:"column:relative_path;type:varchar(512);not null"` + SizeBytes int64 `json:"size_bytes" gorm:"column:size_bytes;type:bigint;not null"` + SHA256 string `json:"sha256" gorm:"column:sha256;type:char(64);not null"` + ReasonCode string `json:"reason_code" gorm:"column:reason_code;type:varchar(64);not null;index:idx_repository_quarantine_reason"` + ReasonMessage string `json:"reason_message" gorm:"column:reason_message;type:varchar(2048);not null"` + DetectedNEVRA string `json:"detected_nevra" gorm:"column:detected_nevra;type:varchar(1024);not null;default:''"` + CreatedAt time.Time `json:"created_at" gorm:"<-:create;autoCreateTime;column:created_at;type:datetime(3);not null"` +} + +func (RepositoryQuarantineItem) TableName() string { return "repository_quarantine_items" } + +// RepositoryGPGKey records an authorized public key. Key material remains in +// the repository bind mount; the database contains metadata only. +type RepositoryGPGKey struct { + ID string `json:"id" gorm:"column:id;type:char(36);primaryKey"` + RepositoryID string `json:"repository_id" gorm:"column:repository_id;type:char(36);not null;uniqueIndex:uniq_repository_gpg_key_fingerprint,priority:1;index:idx_repository_gpg_keys_repository"` + KeyID string `json:"key_id" gorm:"column:key_id;type:varchar(32);not null"` + Fingerprint string `json:"fingerprint" gorm:"column:fingerprint;type:varchar(64);not null;uniqueIndex:uniq_repository_gpg_key_fingerprint,priority:2"` + PublicKeyPath string `json:"public_key_path" gorm:"column:public_key_path;type:varchar(512);not null"` + CreatedAt time.Time `json:"created_at" gorm:"<-:create;autoCreateTime;column:created_at;type:datetime(3);not null"` +} + +func (RepositoryGPGKey) TableName() string { return "repository_gpg_keys" } diff --git a/manager/backend/services/footstone/domain/repository/po/repository_distribution.go b/manager/backend/services/footstone/domain/repository/po/repository_distribution.go new file mode 100644 index 0000000000000000000000000000000000000000..9ad1a176485a68bd783176116c6809730fc3e5ba --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/po/repository_distribution.go @@ -0,0 +1,48 @@ +package po + +import "time" + +type RepositoryDistribution struct { + ID string `json:"id" gorm:"column:id;type:char(36);primaryKey"` + RepositoryID string `json:"repository_id" gorm:"column:repository_id;type:char(36);not null;index:idx_repository_distributions_repository,priority:1"` + ProjectID int64 `json:"project_id" gorm:"column:project_id;type:bigint;not null"` + RevisionID string `json:"revision_id" gorm:"column:revision_id;type:char(36);not null;index:idx_repository_distributions_revision"` + RepositoryTaskID string `json:"repository_task_id" gorm:"column:repository_task_id;type:char(36);not null;index:idx_repository_distributions_task"` + TMSTaskID string `json:"tms_task_id" gorm:"column:tms_task_id;type:varchar(64);not null;default:'';index:idx_repository_distributions_tms_task"` + ArtifactMD5 string `json:"artifact_md5" gorm:"column:artifact_md5;type:char(32);not null;default:''"` + ManifestSHA256 string `json:"manifest_sha256" gorm:"column:manifest_sha256;type:char(64);not null"` + RepomdSHA256 string `json:"repomd_sha256" gorm:"column:repomd_sha256;type:char(64);not null"` + Status string `json:"status" gorm:"column:status;type:varchar(32);not null"` + RequestedBy string `json:"requested_by" gorm:"column:requested_by;type:varchar(256);not null"` + TimeoutSeconds int `json:"timeout_seconds" gorm:"column:timeout_seconds;type:int;not null"` + ErrorCode string `json:"error_code" gorm:"column:error_code;type:varchar(64);not null;default:''"` + ErrorMessage string `json:"error_message" gorm:"column:error_message;type:varchar(2048);not null;default:''"` + CreatedAt time.Time `json:"created_at" gorm:"<-:create;autoCreateTime;column:created_at;type:datetime(3);not null"` + StartedAt *time.Time `json:"started_at" gorm:"column:started_at;type:datetime(3)"` + FinishedAt *time.Time `json:"finished_at" gorm:"column:finished_at;type:datetime(3)"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;column:updated_at;type:datetime(3);not null"` + Targets []RepositoryDistributionTarget `json:"targets" gorm:"-"` +} + +func (RepositoryDistribution) TableName() string { return "repository_distributions" } + +type RepositoryDistributionTarget struct { + ID string `json:"id" gorm:"column:id;type:char(36);primaryKey"` + DistributionID string `json:"distribution_id" gorm:"column:distribution_id;type:char(36);not null;uniqueIndex:uniq_repository_distribution_target,priority:1"` + TargetIP string `json:"target_ip" gorm:"column:target_ip;type:varchar(64);not null;uniqueIndex:uniq_repository_distribution_target,priority:2"` + AgentID uint64 `json:"agent_id" gorm:"column:agent_id;type:bigint unsigned;not null;default:0"` + Status string `json:"status" gorm:"column:status;type:varchar(32);not null"` + AttemptCount int `json:"attempt_count" gorm:"column:attempt_count;type:int;not null;default:0"` + ExitCode int32 `json:"exit_code" gorm:"column:exit_code;type:int;not null;default:0"` + Output string `json:"output" gorm:"column:output;type:mediumtext;not null"` + OutputRef string `json:"output_ref" gorm:"column:output_ref;type:varchar(128);not null;default:''"` + OutputTruncated bool `json:"output_truncated" gorm:"column:output_truncated;type:tinyint(1);not null;default:0"` + ErrorCode string `json:"error_code" gorm:"column:error_code;type:varchar(64);not null;default:''"` + ErrorMessage string `json:"error_message" gorm:"column:error_message;type:varchar(2048);not null;default:''"` + StartedAt *time.Time `json:"started_at" gorm:"column:started_at;type:datetime(3)"` + FinishedAt *time.Time `json:"finished_at" gorm:"column:finished_at;type:datetime(3)"` + CreatedAt time.Time `json:"created_at" gorm:"<-:create;autoCreateTime;column:created_at;type:datetime(3);not null"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;column:updated_at;type:datetime(3);not null"` +} + +func (RepositoryDistributionTarget) TableName() string { return "repository_distribution_targets" } diff --git a/manager/backend/services/footstone/domain/repository/po/repository_inventory.go b/manager/backend/services/footstone/domain/repository/po/repository_inventory.go new file mode 100644 index 0000000000000000000000000000000000000000..f62d2b0eb485bf3ebc3a0ee97b25f4000184eb6d --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/po/repository_inventory.go @@ -0,0 +1,25 @@ +package po + +import "time" + +// RepositoryInventoryImportJob is an audit record for a confirmed inventory +// import. It stores mappings and bounded results, never imported file content +// or credentials. +type RepositoryInventoryImportJob struct { + ID string `json:"id" gorm:"column:id;type:char(36);primaryKey"` + ProjectID int64 `json:"project_id" gorm:"column:project_id;type:bigint;not null;index:idx_repository_inventory_import_project,priority:1"` + PreviewID string `json:"preview_id" gorm:"column:preview_id;type:char(36);not null;uniqueIndex:uniq_repository_inventory_import_preview"` + RepositoryTaskID string `json:"repository_task_id" gorm:"column:repository_task_id;type:char(36);uniqueIndex:uniq_repository_inventory_import_task"` + SchemaVersion string `json:"schema_version" gorm:"column:schema_version;type:varchar(64);not null"` + ConflictStrategy string `json:"conflict_strategy" gorm:"column:conflict_strategy;type:varchar(32);not null"` + Status string `json:"status" gorm:"column:status;type:varchar(32);not null;index:idx_repository_inventory_import_status,priority:1"` + MappingJSON []byte `json:"-" gorm:"column:mapping_json;type:json;not null"` + SummaryJSON []byte `json:"-" gorm:"column:summary_json;type:json;not null"` + ErrorCode string `json:"error_code" gorm:"column:error_code;type:varchar(64);not null;default:''"` + ErrorMessage string `json:"error_message" gorm:"column:error_message;type:varchar(2048);not null;default:''"` + RequestedBy string `json:"requested_by" gorm:"column:requested_by;type:varchar(256);not null"` + CreatedAt time.Time `json:"created_at" gorm:"<-:create;autoCreateTime;column:created_at;type:datetime(3);not null"` + FinishedAt *time.Time `json:"finished_at" gorm:"column:finished_at;type:datetime(3)"` +} + +func (RepositoryInventoryImportJob) TableName() string { return "repository_inventory_import_jobs" } diff --git a/manager/backend/services/footstone/domain/repository/po/repository_node.go b/manager/backend/services/footstone/domain/repository/po/repository_node.go new file mode 100644 index 0000000000000000000000000000000000000000..e4b855df69cd2e4634f802e8d113f15210dcc582 --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/po/repository_node.go @@ -0,0 +1,151 @@ +package po + +import "time" + +// RepositoryStorageNode stores control-plane metadata for a repository data node. +// Client certificates and private keys are referenced by deployment configuration, +// never persisted in this table. +type RepositoryStorageNode struct { + ID string `json:"id" gorm:"column:id;type:char(36);primaryKey"` + ProjectID int64 `json:"project_id" gorm:"column:project_id;type:bigint;not null;index:idx_repository_storage_nodes_project;uniqueIndex:uniq_repository_storage_node_name,priority:1"` + Name string `json:"name" gorm:"column:name;type:varchar(128);not null;uniqueIndex:uniq_repository_storage_node_name,priority:2"` + Description string `json:"description" gorm:"column:description;type:varchar(1024);not null;default:''"` + ControlBaseURL string `json:"control_base_url" gorm:"column:control_base_url;type:varchar(2048);not null"` + PublishBaseURL string `json:"publish_base_url" gorm:"column:publish_base_url;type:varchar(2048);not null"` + TLSServerName string `json:"tls_server_name" gorm:"column:tls_server_name;type:varchar(255);not null;default:''"` + CertificateFingerprint string `json:"certificate_fingerprint" gorm:"column:certificate_fingerprint;type:char(64);not null;default:''"` + Status string `json:"status" gorm:"column:status;type:varchar(32);not null;index:idx_repository_storage_nodes_status"` + Enabled bool `json:"enabled" gorm:"column:enabled;type:tinyint(1);not null;default:1"` + FilesystemType string `json:"filesystem_type" gorm:"column:filesystem_type;type:varchar(32);not null;default:''"` + CapacityBytes int64 `json:"capacity_bytes" gorm:"column:capacity_bytes;type:bigint;not null;default:0"` + FreeBytes int64 `json:"free_bytes" gorm:"column:free_bytes;type:bigint;not null;default:0"` + FreeInodes int64 `json:"free_inodes" gorm:"column:free_inodes;type:bigint;not null;default:0"` + ToolVersionsJSON []byte `json:"-" gorm:"column:tool_versions;type:json"` + LabelsJSON []byte `json:"-" gorm:"column:labels;type:json"` + LastHeartbeatAt *time.Time `json:"last_heartbeat_at" gorm:"column:last_heartbeat_at;type:datetime(3);index:idx_repository_storage_nodes_heartbeat"` + LastProbeAt *time.Time `json:"last_probe_at" gorm:"column:last_probe_at;type:datetime(3)"` + LastErrorCode string `json:"last_error_code" gorm:"column:last_error_code;type:varchar(64);not null;default:''"` + LastErrorMessage string `json:"last_error_message" gorm:"column:last_error_message;type:varchar(2048);not null;default:''"` + Hostname string `json:"hostname" gorm:"column:hostname;type:varchar(255);not null;default:''"` + IPAddress string `json:"ip_address" gorm:"column:ip_address;type:varchar(64);not null;default:''"` + Architecture string `json:"architecture" gorm:"column:architecture;type:varchar(32);not null;default:''"` + OSName string `json:"os_name" gorm:"column:os_name;type:varchar(128);not null;default:''"` + OSVersion string `json:"os_version" gorm:"column:os_version;type:varchar(64);not null;default:''"` + AgentVersion string `json:"agent_version" gorm:"column:agent_version;type:varchar(64);not null;default:''"` + CreatedBy string `json:"created_by" gorm:"column:created_by;type:varchar(256);not null"` + CreatedAt time.Time `json:"created_at" gorm:"<-:create;autoCreateTime;column:created_at;type:datetime(3);not null"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;column:updated_at;type:datetime(3);not null"` + + RepositoryCount int64 `json:"repository_count" gorm:"-"` + RepositoryPhysicalBytes int64 `json:"repository_physical_bytes" gorm:"-"` + Repositories []RepositoryNodeRepositorySummary `json:"repositories" gorm:"-"` + MetadataSigningKeys []RepositoryMetadataSigningKey `json:"metadata_signing_keys" gorm:"-"` +} + +func (RepositoryStorageNode) TableName() string { return "repository_storage_nodes" } + +type RepositoryNodeRepositoryUsage struct { + StorageNodeID string `json:"storage_node_id" gorm:"column:storage_node_id;type:char(36);primaryKey"` + RepositoryID string `json:"repository_id" gorm:"column:repository_id;type:char(36);primaryKey"` + LogicalBytes int64 `json:"logical_bytes" gorm:"column:logical_bytes;type:bigint;not null;default:0"` + PhysicalBytes int64 `json:"physical_bytes" gorm:"column:physical_bytes;type:bigint;not null;default:0"` + PublishStatus string `json:"publish_status" gorm:"column:publish_status;type:varchar(32);not null;default:''"` + ObservedAt time.Time `json:"observed_at" gorm:"column:observed_at;type:datetime(3);not null"` +} + +// RepositoryMetadataSigningKey is the public inventory reported by a storage +// node. It contains no private key material or private key path. +type RepositoryMetadataSigningKey struct { + ID string `json:"id" gorm:"column:id;type:char(36);primaryKey"` + StorageNodeID string `json:"storage_node_id" gorm:"column:storage_node_id;type:char(36);not null;uniqueIndex:uniq_repository_metadata_key_ref,priority:1;index:idx_repository_metadata_keys_status,priority:1"` + KeyRef string `json:"key_ref" gorm:"column:key_ref;type:varchar(128);not null;uniqueIndex:uniq_repository_metadata_key_ref,priority:2"` + Fingerprint string `json:"fingerprint" gorm:"column:fingerprint;type:varchar(64);not null;index:idx_repository_metadata_keys_fingerprint"` + PublicKeyName string `json:"public_key_name" gorm:"column:public_key_name;type:varchar(255);not null"` + PublicKeyURL string `json:"public_key_url" gorm:"column:public_key_url;type:varchar(2048);not null;default:''"` + Status string `json:"status" gorm:"column:status;type:varchar(32);not null;index:idx_repository_metadata_keys_status,priority:2"` + KeyCreatedAt *time.Time `json:"key_created_at" gorm:"column:key_created_at;type:datetime(3)"` + ObservedAt time.Time `json:"observed_at" gorm:"column:observed_at;type:datetime(3);not null"` + CreatedAt time.Time `json:"created_at" gorm:"<-:create;autoCreateTime;column:created_at;type:datetime(3);not null"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;column:updated_at;type:datetime(3);not null"` +} + +func (RepositoryMetadataSigningKey) TableName() string { return "repository_metadata_signing_keys" } + +func (RepositoryNodeRepositoryUsage) TableName() string { return "repository_node_repository_usages" } + +type RepositoryNodeRepositorySummary struct { + RepositoryID string + Name string + SystemProduct string + ProductVersion string + RepositoryType string + Architecture string + UpstreamURL string + PackageCount int64 + LogicalBytes int64 + PhysicalBytes int64 + CurrentRevision string + PublishStatus string + LastSyncAt *time.Time + LastSyncStatus string + NextSyncAt *time.Time + RunningTaskID string + LastSyncErrorCode string + LastSyncErrorMessage string +} + +type RepositoryPreflightRecord struct { + ID string `json:"id" gorm:"column:id;type:char(36);primaryKey"` + StorageNodeID string `json:"storage_node_id" gorm:"column:storage_node_id;type:char(36);not null;index:idx_repository_preflights_node,priority:1"` + RepositoryID string `json:"repository_id" gorm:"column:repository_id;type:char(36);not null;default:'';index:idx_repository_preflights_repository"` + TaskID string `json:"task_id" gorm:"column:task_id;type:char(36);not null;default:'';index:idx_repository_preflights_task"` + Status string `json:"status" gorm:"column:status;type:varchar(32);not null"` + FilesystemType string `json:"filesystem_type" gorm:"column:filesystem_type;type:varchar(32);not null;default:''"` + CapacityBytes int64 `json:"capacity_bytes" gorm:"column:capacity_bytes;type:bigint;not null;default:0"` + FreeBytes int64 `json:"free_bytes" gorm:"column:free_bytes;type:bigint;not null;default:0"` + RequiredFreeBytes int64 `json:"required_free_bytes" gorm:"column:required_free_bytes;type:bigint;not null;default:0"` + FreeInodes int64 `json:"free_inodes" gorm:"column:free_inodes;type:bigint;not null;default:0"` + RequiredInodes int64 `json:"required_inodes" gorm:"column:required_inodes;type:bigint;not null;default:0"` + EstimatedPackageCount int64 `json:"estimated_package_count" gorm:"column:estimated_package_count;type:bigint;not null;default:0"` + EstimatedImportBytes int64 `json:"estimated_import_bytes" gorm:"column:estimated_import_bytes;type:bigint;not null;default:0"` + ToolVersionsJSON []byte `json:"-" gorm:"column:tool_versions;type:json"` + ErrorCode string `json:"error_code" gorm:"column:error_code;type:varchar(64);not null;default:''"` + ErrorMessage string `json:"error_message" gorm:"column:error_message;type:varchar(2048);not null;default:''"` + ExpiresAt time.Time `json:"expires_at" gorm:"column:expires_at;type:datetime(3);not null;index:idx_repository_preflights_node,priority:2"` + CreatedAt time.Time `json:"created_at" gorm:"<-:create;autoCreateTime;column:created_at;type:datetime(3);not null"` +} + +func (RepositoryPreflightRecord) TableName() string { return "repository_preflight_records" } + +type RepositoryTaskLogIndex struct { + ID string `json:"id" gorm:"column:id;type:char(36);primaryKey"` + TaskID string `json:"task_id" gorm:"column:task_id;type:char(36);not null;uniqueIndex:uniq_repository_task_log_index"` + StorageNodeID string `json:"storage_node_id" gorm:"column:storage_node_id;type:char(36);not null;index:idx_repository_task_logs_node"` + RelativePath string `json:"relative_path" gorm:"column:relative_path;type:varchar(512);not null"` + SHA256 string `json:"sha256" gorm:"column:sha256;type:char(64);not null"` + CompressedBytes int64 `json:"compressed_bytes" gorm:"column:compressed_bytes;type:bigint;not null"` + UncompressedBytes int64 `json:"uncompressed_bytes" gorm:"column:uncompressed_bytes;type:bigint;not null"` + LineCount int64 `json:"line_count" gorm:"column:line_count;type:bigint;not null"` + Summary string `json:"summary" gorm:"column:summary;type:text;not null"` + ErrorFingerprint string `json:"error_fingerprint" gorm:"column:error_fingerprint;type:char(64);not null;default:'';index:idx_repository_task_logs_fingerprint"` + ExpiresAt time.Time `json:"expires_at" gorm:"column:expires_at;type:datetime(3);not null;index:idx_repository_task_logs_expires"` + DeletedAt *time.Time `json:"deleted_at" gorm:"column:deleted_at;type:datetime(3)"` + CreatedAt time.Time `json:"created_at" gorm:"<-:create;autoCreateTime;column:created_at;type:datetime(3);not null"` +} + +func (RepositoryTaskLogIndex) TableName() string { return "repository_task_log_indexes" } + +type RepositoryAIAnalysis struct { + ID string `json:"id" gorm:"column:id;type:char(36);primaryKey"` + TaskID string `json:"task_id" gorm:"column:task_id;type:char(36);not null;index:idx_repository_ai_analyses_task"` + SessionID string `json:"session_id" gorm:"column:session_id;type:varchar(128);not null;uniqueIndex:uniq_repository_ai_session"` + ContextSHA256 string `json:"context_sha256" gorm:"column:context_sha256;type:char(64);not null"` + Status string `json:"status" gorm:"column:status;type:varchar(32);not null"` + ActionTaskID string `json:"action_task_id" gorm:"column:action_task_id;type:char(36);not null;default:''"` + ActionResult string `json:"action_result" gorm:"column:action_result;type:varchar(32);not null;default:''"` + RequestedBy string `json:"requested_by" gorm:"column:requested_by;type:varchar(256);not null"` + CreatedAt time.Time `json:"created_at" gorm:"<-:create;autoCreateTime;column:created_at;type:datetime(3);not null"` + FeedbackAt *time.Time `json:"feedback_at" gorm:"column:feedback_at;type:datetime(3)"` +} + +func (RepositoryAIAnalysis) TableName() string { return "repository_ai_analyses" } diff --git a/manager/backend/services/footstone/domain/repository/po/repository_node_lifecycle.go b/manager/backend/services/footstone/domain/repository/po/repository_node_lifecycle.go new file mode 100644 index 0000000000000000000000000000000000000000..0dfa386d6a3929c9e34b2dd9d8da0b16a1ae0c19 --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/po/repository_node_lifecycle.go @@ -0,0 +1,115 @@ +package po + +import "time" + +// RepositoryTaskTarget stores the per-node result of one logical business task. +type RepositoryTaskTarget struct { + ID string `json:"id" gorm:"column:id;type:char(36);primaryKey"` + TaskID string `json:"task_id" gorm:"column:task_id;type:char(36);not null;uniqueIndex:uniq_repository_task_target,priority:1;index:idx_repository_task_targets_status,priority:1"` + TargetType string `json:"target_type" gorm:"column:target_type;type:varchar(32);not null;uniqueIndex:uniq_repository_task_target,priority:2"` + TargetID string `json:"target_id" gorm:"column:target_id;type:varchar(128);not null;uniqueIndex:uniq_repository_task_target,priority:3"` + StorageNodeID string `json:"storage_node_id" gorm:"column:storage_node_id;type:char(36);not null;default:'';index:idx_repository_task_targets_node"` + TMSAgentID uint64 `json:"tms_agent_id" gorm:"column:tms_agent_id;type:bigint unsigned;not null;default:0;index:idx_repository_task_targets_agent"` + Status string `json:"status" gorm:"column:status;type:varchar(32);not null;index:idx_repository_task_targets_status,priority:2"` + ProgressPercent int32 `json:"progress_percent" gorm:"column:progress_percent;type:int;not null;default:0"` + StartedAt *time.Time `json:"started_at" gorm:"column:started_at;type:datetime(3)"` + FinishedAt *time.Time `json:"finished_at" gorm:"column:finished_at;type:datetime(3)"` + ErrorCode string `json:"error_code" gorm:"column:error_code;type:varchar(64);not null;default:''"` + ErrorMessage string `json:"error_message" gorm:"column:error_message;type:varchar(2048);not null;default:''"` + ResultJSON []byte `json:"-" gorm:"column:result_json;type:json"` + CreatedAt time.Time `json:"created_at" gorm:"<-:create;autoCreateTime;column:created_at;type:datetime(3);not null"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;column:updated_at;type:datetime(3);not null"` +} + +func (RepositoryTaskTarget) TableName() string { return "repository_task_targets" } + +// RepositoryNodeEnrollment is the stable identity allocated before node software is installed. +type RepositoryNodeEnrollment struct { + ID string `json:"id" gorm:"column:id;type:char(36);primaryKey"` + NodeID string `json:"node_id" gorm:"column:node_id;type:char(36);not null;index:idx_repository_node_enrollments_node"` + ProjectID int64 `json:"project_id" gorm:"column:project_id;type:bigint;not null;index:idx_repository_node_enrollments_project"` + TMSAgentID uint64 `json:"tms_agent_id" gorm:"column:tms_agent_id;type:bigint unsigned;not null;default:0;index:idx_repository_node_enrollments_agent"` + Generation int `json:"generation" gorm:"column:generation;type:int;not null"` + Status string `json:"status" gorm:"column:status;type:varchar(32);not null;index:idx_repository_node_enrollments_status"` + InstallTaskID string `json:"install_task_id" gorm:"column:install_task_id;type:char(36);not null;index:idx_repository_node_enrollments_task"` + TMSTaskID string `json:"tms_task_id" gorm:"column:tms_task_id;type:varchar(64);not null;default:'';index:idx_repository_node_enrollments_tms_task"` + AllowlistEntryID string `json:"allowlist_entry_id" gorm:"column:allowlist_entry_id;type:char(36);index:idx_repository_node_enrollments_allowlist"` + ArtifactID string `json:"artifact_id" gorm:"column:artifact_id;type:char(36);index:idx_repository_node_enrollments_artifact"` + AuthorizationID *string `json:"authorization_id,omitempty" gorm:"column:authorization_id;type:char(36);uniqueIndex:uniq_repository_node_enrollment_authorization"` + CSRSHA256 string `json:"csr_sha256" gorm:"column:csr_sha256;type:char(64);not null;default:''"` + AuthorizedAt *time.Time `json:"authorized_at" gorm:"column:authorized_at;type:datetime(3)"` + IssuanceStatus string `json:"issuance_status" gorm:"column:issuance_status;type:varchar(32);not null;default:'PENDING';index:idx_repository_node_enrollments_issuance,priority:1"` + IssuanceLeaseOwner string `json:"issuance_lease_owner" gorm:"column:issuance_lease_owner;type:varchar(128);not null;default:''"` + IssuanceLeaseExpiresAt *time.Time `json:"issuance_lease_expires_at" gorm:"column:issuance_lease_expires_at;type:datetime(3);index:idx_repository_node_enrollments_issuance,priority:2"` + ExpectedArchitecture string `json:"expected_architecture" gorm:"column:expected_architecture;type:varchar(32);not null"` + ExpiresAt time.Time `json:"expires_at" gorm:"column:expires_at;type:datetime(3);not null;index:idx_repository_node_enrollments_expires"` + LastErrorCode string `json:"last_error_code" gorm:"column:last_error_code;type:varchar(64);not null;default:''"` + LastErrorMessage string `json:"last_error_message" gorm:"column:last_error_message;type:varchar(2048);not null;default:''"` + CreatedBy string `json:"created_by" gorm:"column:created_by;type:varchar(256);not null"` + CreatedAt time.Time `json:"created_at" gorm:"<-:create;autoCreateTime;column:created_at;type:datetime(3);not null"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;column:updated_at;type:datetime(3);not null"` +} + +func (RepositoryNodeEnrollment) TableName() string { return "repository_node_enrollments" } + +// RepositoryNodeBootstrapToken stores only a SHA-256 digest of the one-time token. +type RepositoryNodeBootstrapToken struct { + ID string `json:"id" gorm:"column:id;type:char(36);primaryKey"` + EnrollmentID string `json:"enrollment_id" gorm:"column:enrollment_id;type:char(36);not null;uniqueIndex:uniq_repository_node_bootstrap_generation,priority:1"` + Generation int `json:"generation" gorm:"column:generation;type:int;not null;uniqueIndex:uniq_repository_node_bootstrap_generation,priority:2"` + TaskID string `json:"task_id" gorm:"column:task_id;type:char(36);not null;index:idx_repository_node_bootstrap_task"` + TMSAgentID uint64 `json:"tms_agent_id" gorm:"column:tms_agent_id;type:bigint unsigned;not null;default:0"` + TokenHash string `json:"-" gorm:"column:token_hash;type:char(64);not null;uniqueIndex:uniq_repository_node_bootstrap_hash"` + ExpiresAt time.Time `json:"expires_at" gorm:"column:expires_at;type:datetime(3);not null;index:idx_repository_node_bootstrap_expires"` + ConsumedAt *time.Time `json:"consumed_at" gorm:"column:consumed_at;type:datetime(3)"` + InvalidatedAt *time.Time `json:"invalidated_at" gorm:"column:invalidated_at;type:datetime(3)"` + CreatedAt time.Time `json:"created_at" gorm:"<-:create;autoCreateTime;column:created_at;type:datetime(3);not null"` +} + +func (RepositoryNodeBootstrapToken) TableName() string { return "repository_node_bootstrap_tokens" } + +// RepositoryNodeCertificate records public certificate identity and lifecycle only. +type RepositoryNodeCertificate struct { + ID string `json:"id" gorm:"column:id;type:char(36);primaryKey"` + NodeID string `json:"node_id" gorm:"column:node_id;type:char(36);not null;index:idx_repository_node_certificates_node,priority:1"` + EnrollmentID string `json:"enrollment_id" gorm:"column:enrollment_id;type:char(36);not null;default:''"` + AuthorizationID string `json:"authorization_id" gorm:"column:authorization_id;type:char(36);uniqueIndex:uniq_repository_node_certificate_authorization"` + SerialNumber string `json:"serial_number" gorm:"column:serial_number;type:varchar(128);not null;uniqueIndex:uniq_repository_node_certificate_serial"` + Fingerprint string `json:"fingerprint" gorm:"column:fingerprint;type:char(64);not null;uniqueIndex:uniq_repository_node_certificate_fingerprint"` + CertificatePEM string `json:"certificate_pem" gorm:"column:certificate_pem;type:mediumtext"` + CertificateChainPEM string `json:"certificate_chain_pem" gorm:"column:certificate_chain_pem;type:mediumtext"` + IssuerFingerprint string `json:"issuer_fingerprint" gorm:"column:issuer_fingerprint;type:char(64);not null;default:''"` + Status string `json:"status" gorm:"column:status;type:varchar(32);not null;index:idx_repository_node_certificates_node,priority:2"` + NotBefore time.Time `json:"not_before" gorm:"column:not_before;type:datetime(3);not null"` + NotAfter time.Time `json:"not_after" gorm:"column:not_after;type:datetime(3);not null;index:idx_repository_node_certificates_expiry"` + GraceUntil *time.Time `json:"grace_until" gorm:"column:grace_until;type:datetime(3)"` + CreatedAt time.Time `json:"created_at" gorm:"<-:create;autoCreateTime;column:created_at;type:datetime(3);not null"` +} + +func (RepositoryNodeCertificate) TableName() string { return "repository_node_certificates" } + +// RepositoryNodeArtifact identifies a versioned, immutable node RPM. +type RepositoryNodeArtifact struct { + ID string `json:"id" gorm:"column:id;type:char(36);primaryKey"` + ProjectID int64 `json:"project_id" gorm:"column:project_id;type:bigint;not null;default:0;index:idx_repository_node_artifacts_selection,priority:1;uniqueIndex:uniq_repository_node_artifact_evr_source,priority:1"` + SourceKind string `json:"source_kind" gorm:"column:source_kind;type:varchar(32);not null;default:'LEGACY_LOCAL';index:idx_repository_node_artifacts_selection,priority:3"` + SourceRepositoryID string `json:"source_repository_id" gorm:"column:source_repository_id;type:char(36);uniqueIndex:uniq_repository_node_artifact_evr_source,priority:2"` + RevisionID string `json:"revision_id" gorm:"column:revision_id;type:char(36);index:idx_repository_node_artifacts_revision,priority:1"` + PackageID string `json:"package_id" gorm:"column:package_id;type:char(36);index:idx_repository_node_artifacts_revision,priority:2"` + Name string `json:"name" gorm:"column:name;type:varchar(128);not null;uniqueIndex:uniq_repository_node_artifact_evr_source,priority:3"` + Epoch uint64 `json:"epoch" gorm:"column:epoch;type:bigint unsigned;not null;default:0;uniqueIndex:uniq_repository_node_artifact_evr_source,priority:4"` + Version string `json:"version" gorm:"column:version;type:varchar(64);not null;uniqueIndex:uniq_repository_node_artifact_evr_source,priority:5"` + Release string `json:"release" gorm:"column:release;type:varchar(32);not null;uniqueIndex:uniq_repository_node_artifact_evr_source,priority:6"` + Architecture string `json:"architecture" gorm:"column:architecture;type:varchar(32);not null;index:idx_repository_node_artifacts_selection,priority:2;uniqueIndex:uniq_repository_node_artifact_evr_source,priority:7"` + NEVRA string `json:"nevra" gorm:"column:nevra;type:varchar(512);not null;default:''"` + FileName string `json:"file_name" gorm:"column:file_name;type:varchar(255);not null"` + SizeBytes int64 `json:"size_bytes" gorm:"column:size_bytes;type:bigint;not null"` + SHA256 string `json:"sha256" gorm:"column:sha256;type:char(64);not null"` + BuildCommit string `json:"build_commit" gorm:"column:build_commit;type:varchar(64);not null"` + RelativePath string `json:"relative_path" gorm:"column:relative_path;type:varchar(512);not null"` + ImmutableURL string `json:"immutable_url" gorm:"column:immutable_url;type:varchar(2048);not null;default:''"` + Status string `json:"status" gorm:"column:status;type:varchar(32);not null;index:idx_repository_node_artifacts_status;index:idx_repository_node_artifacts_selection,priority:4"` + CreatedAt time.Time `json:"created_at" gorm:"<-:create;autoCreateTime;column:created_at;type:datetime(3);not null"` +} + +func (RepositoryNodeArtifact) TableName() string { return "repository_node_artifacts" } diff --git a/manager/backend/services/footstone/domain/repository/po/repository_package_install.go b/manager/backend/services/footstone/domain/repository/po/repository_package_install.go new file mode 100644 index 0000000000000000000000000000000000000000..068da169f175911b041ff57e68b115c118781a74 --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/po/repository_package_install.go @@ -0,0 +1,43 @@ +package po + +import "time" + +// RepositoryOperationPreview stores the short-lived, user-bound result of a +// Preview/Confirm operation. Payload contains public repository configuration +// and bounded command output, never credentials. +type RepositoryOperationPreview struct { + ID string `json:"id" gorm:"column:id;type:char(36);primaryKey"` + ProjectID int64 `json:"project_id" gorm:"column:project_id;type:bigint;not null;index:idx_repository_operation_previews_resource,priority:1"` + OperationType string `json:"operation_type" gorm:"column:operation_type;type:varchar(32);not null;index:idx_repository_operation_previews_resource,priority:2"` + ResourceID string `json:"resource_id" gorm:"column:resource_id;type:varchar(128);not null;index:idx_repository_operation_previews_resource,priority:3"` + ResourceVersion string `json:"resource_version" gorm:"column:resource_version;type:varchar(128);not null"` + PreviewDigest string `json:"preview_digest" gorm:"column:preview_digest;type:char(64);not null;uniqueIndex:uniq_repository_operation_preview_digest"` + PayloadJSON []byte `json:"-" gorm:"column:payload;type:json;not null"` + Status string `json:"status" gorm:"column:status;type:varchar(32);not null;index:idx_repository_operation_previews_expiry,priority:1"` + ExpiresAt time.Time `json:"expires_at" gorm:"column:expires_at;type:datetime(3);not null;index:idx_repository_operation_previews_expiry,priority:2"` + CreatedBy string `json:"created_by" gorm:"column:created_by;type:varchar(256);not null"` + CreatedAt time.Time `json:"created_at" gorm:"<-:create;autoCreateTime;column:created_at;type:datetime(3);not null"` + ConfirmedAt *time.Time `json:"confirmed_at" gorm:"column:confirmed_at;type:datetime(3)"` +} + +func (RepositoryOperationPreview) TableName() string { return "repository_operation_previews" } + +// RepositoryPackageInstall links one logical PACKAGE_INSTALL task to the TMS +// operation that executes it on one or more targets. +type RepositoryPackageInstall struct { + ID string `json:"id" gorm:"column:id;type:char(36);primaryKey"` + ProjectID int64 `json:"project_id" gorm:"column:project_id;type:bigint;not null;index:idx_repository_package_installs_project"` + RepositoryID string `json:"repository_id" gorm:"column:repository_id;type:char(36);not null;index:idx_repository_package_installs_repository"` + RevisionID string `json:"revision_id" gorm:"column:revision_id;type:char(36);not null"` + PreviewID string `json:"preview_id" gorm:"column:preview_id;type:char(36);not null;uniqueIndex:uniq_repository_package_install_preview"` + RepositoryTaskID string `json:"repository_task_id" gorm:"column:repository_task_id;type:char(36);not null;uniqueIndex:uniq_repository_package_install_task"` + TMSTaskID string `json:"tms_task_id" gorm:"column:tms_task_id;type:varchar(64);not null;default:'';index:idx_repository_package_installs_tms_task"` + ArtifactMD5 string `json:"artifact_md5" gorm:"column:artifact_md5;type:char(32);not null;default:''"` + Status string `json:"status" gorm:"column:status;type:varchar(32);not null;index:idx_repository_package_installs_status"` + TimeoutSeconds int `json:"timeout_seconds" gorm:"column:timeout_seconds;type:int;not null"` + RequestedBy string `json:"requested_by" gorm:"column:requested_by;type:varchar(256);not null"` + CreatedAt time.Time `json:"created_at" gorm:"<-:create;autoCreateTime;column:created_at;type:datetime(3);not null"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;column:updated_at;type:datetime(3);not null"` +} + +func (RepositoryPackageInstall) TableName() string { return "repository_package_installs" } diff --git a/manager/backend/services/footstone/domain/repository/po/repository_schedule.go b/manager/backend/services/footstone/domain/repository/po/repository_schedule.go new file mode 100644 index 0000000000000000000000000000000000000000..06cfb1d19a2679c4ef86eff1286783849cb8907b --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/po/repository_schedule.go @@ -0,0 +1,43 @@ +package po + +import "time" + +type RepositorySyncSchedule struct { + ID string `json:"id" gorm:"column:id;type:char(36);primaryKey"` + RepositoryID string `json:"repository_id" gorm:"column:repository_id;type:char(36);not null;uniqueIndex:uniq_repository_sync_schedule_repository"` + ProjectID int64 `json:"project_id" gorm:"column:project_id;type:bigint;not null;index:idx_repository_sync_schedules_project"` + Cadence string `json:"cadence" gorm:"column:cadence;type:varchar(16);not null"` + Timezone string `json:"timezone" gorm:"column:timezone;type:varchar(64);not null;default:'UTC'"` + DayOfWeek int `json:"day_of_week" gorm:"column:day_of_week;type:tinyint;not null;default:0"` + Hour int `json:"hour" gorm:"column:hour;type:tinyint;not null;default:0"` + Minute int `json:"minute" gorm:"column:minute;type:tinyint;not null;default:0"` + Status string `json:"status" gorm:"column:status;type:varchar(32);not null;default:'ENABLED';index:idx_repository_sync_schedules_due,priority:1"` + LastRunAt *time.Time `json:"last_run_at" gorm:"column:last_run_at;type:datetime(3)"` + NextRunAt time.Time `json:"next_run_at" gorm:"column:next_run_at;type:datetime(3);not null;index:idx_repository_sync_schedules_due,priority:2"` + ConsecutiveFailures int `json:"consecutive_failures" gorm:"column:consecutive_failures;type:int;not null;default:0"` + FailureThreshold int `json:"failure_threshold" gorm:"column:failure_threshold;type:int;not null;default:3"` + LastErrorCode string `json:"last_error_code" gorm:"column:last_error_code;type:varchar(64);not null;default:''"` + LastErrorMessage string `json:"last_error_message" gorm:"column:last_error_message;type:varchar(2048);not null;default:''"` + CreatedBy string `json:"created_by" gorm:"column:created_by;type:varchar(256);not null"` + CreatedAt time.Time `json:"created_at" gorm:"<-:create;autoCreateTime;column:created_at;type:datetime(3);not null"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;column:updated_at;type:datetime(3);not null"` +} + +func (RepositorySyncSchedule) TableName() string { return "repository_sync_schedules" } + +type RepositoryScheduleRun struct { + ID string `json:"id" gorm:"column:id;type:char(36);primaryKey"` + ScheduleID string `json:"schedule_id" gorm:"column:schedule_id;type:char(36);not null;index:idx_repository_schedule_runs_schedule,priority:1"` + RepositoryID string `json:"repository_id" gorm:"column:repository_id;type:char(36);not null"` + ScheduledAt time.Time `json:"scheduled_at" gorm:"column:scheduled_at;type:datetime(3);not null;index:idx_repository_schedule_runs_schedule,priority:2"` + ScheduleRunKey string `json:"schedule_run_key" gorm:"column:schedule_run_key;type:char(64);not null;uniqueIndex:uniq_repository_schedule_run_key"` + Status string `json:"status" gorm:"column:status;type:varchar(32);not null;index:idx_repository_schedule_runs_status,priority:1"` + TaskID string `json:"task_id" gorm:"column:task_id;type:char(36);not null;default:'';index:idx_repository_schedule_runs_task"` + ErrorCode string `json:"error_code" gorm:"column:error_code;type:varchar(64);not null;default:''"` + ErrorMessage string `json:"error_message" gorm:"column:error_message;type:varchar(2048);not null;default:''"` + FinishedAt *time.Time `json:"finished_at" gorm:"column:finished_at;type:datetime(3)"` + CreatedAt time.Time `json:"created_at" gorm:"<-:create;autoCreateTime;column:created_at;type:datetime(3);not null"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;column:updated_at;type:datetime(3);not null"` +} + +func (RepositoryScheduleRun) TableName() string { return "repository_schedule_runs" } diff --git a/manager/backend/services/footstone/domain/repository/po/repository_task.go b/manager/backend/services/footstone/domain/repository/po/repository_task.go new file mode 100644 index 0000000000000000000000000000000000000000..a8123a8bc614853ee03889d768e9b54d62586a5e --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/po/repository_task.go @@ -0,0 +1,119 @@ +package po + +import ( + "encoding/json" + "time" +) + +// RepositoryTask is the durable control-plane record for asynchronous work. +// Payload is internal-only and must contain references, never credentials or RPM bytes. +type RepositoryTask struct { + ID string `json:"id" gorm:"column:id;type:char(36);primaryKey"` + RootTaskID string `json:"root_task_id" gorm:"column:root_task_id;type:char(36);not null;index:idx_repository_tasks_root"` + RetryOfTaskID string `json:"retry_of_task_id" gorm:"column:retry_of_task_id;type:char(36);not null;default:''"` + RepositoryID string `json:"repository_id" gorm:"column:repository_id;type:char(36);not null;index:idx_repository_tasks_repository,priority:1"` + StorageNodeID string `json:"storage_node_id" gorm:"column:storage_node_id;type:char(36);not null;default:'';index:idx_repository_tasks_node_claim,priority:1"` + PreflightID string `json:"preflight_id" gorm:"column:preflight_id;type:char(36);not null;default:'';index:idx_repository_tasks_preflight"` + ProjectID int64 `json:"project_id" gorm:"column:project_id;type:bigint;not null;index:idx_repository_tasks_project"` + ScopeType string `json:"scope_type" gorm:"column:scope_type;type:varchar(32);not null;default:'REPOSITORY';index:idx_repository_tasks_scope,priority:2"` + ScopeID string `json:"scope_id" gorm:"column:scope_id;type:varchar(128);not null;default:'';index:idx_repository_tasks_scope,priority:3"` + ExecutorKind string `json:"executor_kind" gorm:"column:executor_kind;type:varchar(32);not null;default:'REPOSITORY_WORKER'"` + RevisionID string `json:"revision_id" gorm:"column:revision_id;type:char(36);not null;default:''"` + Type string `json:"type" gorm:"column:task_type;type:varchar(32);not null"` + Trigger string `json:"trigger" gorm:"column:trigger_source;type:varchar(32);not null;default:'MANUAL'"` + IdempotencyKey *string `json:"idempotency_key,omitempty" gorm:"column:idempotency_key;type:varchar(128);uniqueIndex:uniq_repository_task_idempotency"` + BandwidthLimitBytes int64 `json:"bandwidth_limit_bytes" gorm:"column:bandwidth_limit_bytes;type:bigint;not null;default:0"` + MaxParallelDownloads int `json:"max_parallel_downloads" gorm:"column:max_parallel_downloads;type:int;not null;default:0"` + Status string `json:"status" gorm:"column:status;type:varchar(32);not null;index:idx_repository_tasks_claim,priority:1"` + RequiresMutationLock bool `json:"requires_mutation_lock" gorm:"column:requires_mutation_lock;type:tinyint(1);not null"` + Attempt int `json:"attempt" gorm:"column:attempt;type:int;not null;default:0"` + MaxAttempts int `json:"max_attempts" gorm:"column:max_attempts;type:int;not null"` + RequestedBy string `json:"requested_by" gorm:"column:requested_by;type:varchar(256);not null"` + WorkerID string `json:"worker_id" gorm:"column:worker_id;type:varchar(128);not null;default:''"` + FencingToken uint64 `json:"fencing_token" gorm:"column:fencing_token;type:bigint unsigned;not null;default:0"` + LeaseExpiresAt *time.Time `json:"lease_expires_at" gorm:"column:lease_expires_at;type:datetime(3);index:idx_repository_tasks_lease"` + LastHeartbeatAt *time.Time `json:"last_heartbeat_at" gorm:"column:last_heartbeat_at;type:datetime(3)"` + AvailableAt time.Time `json:"available_at" gorm:"column:available_at;type:datetime(3);not null;index:idx_repository_tasks_claim,priority:2"` + StartedAt *time.Time `json:"started_at" gorm:"column:started_at;type:datetime(3)"` + FinishedAt *time.Time `json:"finished_at" gorm:"column:finished_at;type:datetime(3)"` + CancelRequestedAt *time.Time `json:"cancel_requested_at" gorm:"column:cancel_requested_at;type:datetime(3)"` + ProgressPercent int32 `json:"progress_percent" gorm:"column:progress_percent;type:int;not null;default:0"` + CurrentStep string `json:"current_step" gorm:"column:current_step;type:varchar(128);not null;default:''"` + ErrorCode string `json:"error_code" gorm:"column:error_code;type:varchar(64);not null;default:''"` + ErrorMessage string `json:"error_message" gorm:"column:error_message;type:varchar(2048);not null;default:''"` + FailureCategory string `json:"failure_category" gorm:"column:failure_category;type:varchar(64);not null;default:''"` + FailedStep string `json:"failed_step" gorm:"column:failed_step;type:varchar(128);not null;default:''"` + LikelyCause string `json:"likely_cause" gorm:"column:likely_cause;type:varchar(2048);not null;default:''"` + DiagnosisEvidenceJSON []byte `json:"-" gorm:"column:diagnosis_evidence;type:json"` + RecommendedAction string `json:"recommended_action" gorm:"column:recommended_action;type:varchar(2048);not null;default:''"` + DiagnosisRuleID string `json:"diagnosis_rule_id" gorm:"column:diagnosis_rule_id;type:varchar(128);not null;default:''"` + DiagnosisGeneratedAt *time.Time `json:"diagnosis_generated_at" gorm:"column:diagnosis_generated_at;type:datetime(3)"` + PayloadJSON string `json:"-" gorm:"column:payload;type:json;not null"` + CommitState string `json:"commit_state" gorm:"column:commit_state;type:varchar(32);not null;default:'NONE'"` + CommitToken string `json:"commit_token" gorm:"column:commit_token;type:char(64);not null;default:''"` + PreparedRevisionID string `json:"prepared_revision_id" gorm:"column:prepared_revision_id;type:char(36);not null;default:''"` + PreparedManifestSHA256 string `json:"prepared_manifest_sha256" gorm:"column:prepared_manifest_sha256;type:char(64);not null;default:''"` + ResultObjectID string `json:"result_object_id" gorm:"column:result_object_id;type:varchar(512);not null;default:''"` + ResultObjectSHA256 string `json:"result_object_sha256" gorm:"column:result_object_sha256;type:char(64);not null;default:''"` + PreparedCompletionStatus string `json:"prepared_completion_status" gorm:"column:prepared_completion_status;type:varchar(32);not null;default:'SUCCESS'"` + PreparedErrorCode string `json:"prepared_error_code" gorm:"column:prepared_error_code;type:varchar(64);not null;default:''"` + PreparedErrorMessage string `json:"prepared_error_message" gorm:"column:prepared_error_message;type:varchar(2048);not null;default:''"` + PreparedAt *time.Time `json:"prepared_at" gorm:"column:prepared_at;type:datetime(3)"` + ActivatedAt *time.Time `json:"activated_at" gorm:"column:activated_at;type:datetime(3)"` + ConfirmedAt *time.Time `json:"confirmed_at" gorm:"column:confirmed_at;type:datetime(3)"` + CreatedAt time.Time `json:"created_at" gorm:"<-:create;autoCreateTime;column:created_at;type:datetime(3);not null;index:idx_repository_tasks_repository,priority:2;index:idx_repository_tasks_claim,priority:3"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;column:updated_at;type:datetime(3);not null"` + RepositoryName string `json:"repository_name" gorm:"-"` + StorageNodeName string `json:"storage_node_name" gorm:"-"` + Targets []RepositoryTaskTarget `json:"targets" gorm:"-"` + ResultItems []RepositoryTaskResultItem `json:"result_items" gorm:"-"` +} + +func (RepositoryTask) TableName() string { + return "repository_tasks" +} + +// RepositoryDiagnosis is the stable, user-facing explanation attached to a terminal task. +type RepositoryDiagnosis struct { + FailureCategory string `json:"failure_category"` + ErrorCode string `json:"error_code"` + FailedStep string `json:"failed_step"` + LikelyCause string `json:"likely_cause"` + Evidence []string `json:"evidence"` + RecommendedAction string `json:"recommended_action"` + RuleID string `json:"rule_id"` + GeneratedAt time.Time `json:"generated_at"` +} + +func (task *RepositoryTask) StructuredDiagnosis() *RepositoryDiagnosis { + if task == nil || task.FailureCategory == "" || task.DiagnosisGeneratedAt == nil { + return nil + } + evidence := make([]string, 0) + if len(task.DiagnosisEvidenceJSON) > 0 { + _ = json.Unmarshal(task.DiagnosisEvidenceJSON, &evidence) + } + return &RepositoryDiagnosis{ + FailureCategory: task.FailureCategory, ErrorCode: task.ErrorCode, FailedStep: task.FailedStep, + LikelyCause: task.LikelyCause, Evidence: evidence, RecommendedAction: task.RecommendedAction, + RuleID: task.DiagnosisRuleID, GeneratedAt: task.DiagnosisGeneratedAt.UTC(), + } +} + +// RepositoryMutationLock retains its row after release so fencing tokens are monotonic. +type RepositoryMutationLock struct { + RepositoryID string `json:"repository_id" gorm:"column:repository_id;type:char(36);primaryKey"` + OwnerTaskID string `json:"owner_task_id" gorm:"column:owner_task_id;type:char(36);not null;default:''"` + WorkerID string `json:"worker_id" gorm:"column:worker_id;type:varchar(128);not null;default:''"` + FencingToken uint64 `json:"fencing_token" gorm:"column:fencing_token;type:bigint unsigned;not null;default:0"` + LeaseExpiresAt *time.Time `json:"lease_expires_at" gorm:"column:lease_expires_at;type:datetime(3);index:idx_repository_mutation_locks_lease"` + LastHeartbeatAt *time.Time `json:"last_heartbeat_at" gorm:"column:last_heartbeat_at;type:datetime(3)"` + AcquiredAt *time.Time `json:"acquired_at" gorm:"column:acquired_at;type:datetime(3)"` + ReleasedAt *time.Time `json:"released_at" gorm:"column:released_at;type:datetime(3)"` + CreatedAt time.Time `json:"created_at" gorm:"<-:create;autoCreateTime;column:created_at;type:datetime(3);not null"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;column:updated_at;type:datetime(3);not null"` +} + +func (RepositoryMutationLock) TableName() string { + return "repository_mutation_locks" +} diff --git a/manager/backend/services/footstone/domain/repository/po/repository_upstream_profile.go b/manager/backend/services/footstone/domain/repository/po/repository_upstream_profile.go new file mode 100644 index 0000000000000000000000000000000000000000..8421c9e717bc944b47b329cf6a2f08ab89ed2069 --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/po/repository_upstream_profile.go @@ -0,0 +1,69 @@ +package po + +import ( + "encoding/json" + "fmt" + "time" +) + +// RepositoryUpstreamProfile stores reusable source metadata. Repositories keep +// the resolved URL and filters so profile changes cannot alter an active source. +type RepositoryUpstreamProfile struct { + ID string `json:"id" gorm:"column:id;type:char(36);primaryKey"` + ProjectID int64 `json:"project_id" gorm:"column:project_id;type:bigint;not null;index:idx_repository_upstream_profiles_project;index:idx_repository_upstream_profiles_product,priority:1;index:idx_repository_upstream_profiles_enabled,priority:1;uniqueIndex:uniq_repository_upstream_profile_name,priority:1"` + Name string `json:"name" gorm:"column:name;type:varchar(128);not null;uniqueIndex:uniq_repository_upstream_profile_name"` + Description string `json:"description" gorm:"column:description;type:varchar(1024);not null;default:''"` + SystemProduct string `json:"system_product" gorm:"column:system_product;type:varchar(128);not null;index:idx_repository_upstream_profiles_product,priority:2"` + Channel string `json:"channel" gorm:"column:channel;type:varchar(64);not null"` + URLTemplate string `json:"url_template" gorm:"column:url_template;type:varchar(2048);not null"` + SupportedVersionsJSON string `json:"-" gorm:"column:supported_versions;type:json;not null"` + SupportedArchitecturesJSON string `json:"-" gorm:"column:supported_architectures;type:json;not null"` + DefaultIncludePackagesJSON string `json:"-" gorm:"column:default_include_packages;type:json;not null"` + DefaultExcludePackagesJSON string `json:"-" gorm:"column:default_exclude_packages;type:json;not null"` + Enabled bool `json:"enabled" gorm:"column:enabled;type:tinyint(1);not null;default:1;index:idx_repository_upstream_profiles_enabled,priority:2"` + CreatedBy string `json:"created_by" gorm:"column:created_by;type:varchar(256);not null"` + CreatedAt time.Time `json:"created_at" gorm:"<-:create;autoCreateTime;column:created_at;type:datetime(3);not null"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;column:updated_at;type:datetime(3);not null"` +} + +func (RepositoryUpstreamProfile) TableName() string { return "repository_upstream_profiles" } + +func (p *RepositoryUpstreamProfile) SetCollections(versions, architectures, include, exclude []string) error { + values := []struct { + name string + input []string + target *string + }{ + {name: "supported versions", input: versions, target: &p.SupportedVersionsJSON}, + {name: "supported architectures", input: architectures, target: &p.SupportedArchitecturesJSON}, + {name: "default include packages", input: include, target: &p.DefaultIncludePackagesJSON}, + {name: "default exclude packages", input: exclude, target: &p.DefaultExcludePackagesJSON}, + } + for _, value := range values { + encoded, err := json.Marshal(value.input) + if err != nil { + return fmt.Errorf("marshal %s: %w", value.name, err) + } + *value.target = string(encoded) + } + return nil +} + +func (p RepositoryUpstreamProfile) Collections() (versions, architectures, include, exclude []string, err error) { + values := []struct { + name string + input string + target *[]string + }{ + {name: "supported versions", input: p.SupportedVersionsJSON, target: &versions}, + {name: "supported architectures", input: p.SupportedArchitecturesJSON, target: &architectures}, + {name: "default include packages", input: p.DefaultIncludePackagesJSON, target: &include}, + {name: "default exclude packages", input: p.DefaultExcludePackagesJSON, target: &exclude}, + } + for _, value := range values { + if decodeErr := json.Unmarshal([]byte(value.input), value.target); decodeErr != nil { + return nil, nil, nil, nil, fmt.Errorf("decode %s: %w", value.name, decodeErr) + } + } + return versions, architectures, include, exclude, nil +} diff --git a/manager/backend/services/footstone/domain/repository/po/repository_upstream_profile_test.go b/manager/backend/services/footstone/domain/repository/po/repository_upstream_profile_test.go new file mode 100644 index 0000000000000000000000000000000000000000..69700e51df24f30ec8bd5d9bcde8e095eaba939f --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/po/repository_upstream_profile_test.go @@ -0,0 +1,17 @@ +package po + +import "testing" + +func TestRepositoryUpstreamProfileCollections(t *testing.T) { + value := RepositoryUpstreamProfile{} + if err := value.SetCollections([]string{"9.6"}, []string{"x86_64"}, []string{"bash"}, []string{"*-debug"}); err != nil { + t.Fatal(err) + } + versions, architectures, include, exclude, err := value.Collections() + if err != nil { + t.Fatal(err) + } + if versions[0] != "9.6" || architectures[0] != "x86_64" || include[0] != "bash" || exclude[0] != "*-debug" { + t.Fatalf("Collections() = %v/%v/%v/%v", versions, architectures, include, exclude) + } +} diff --git a/manager/backend/services/footstone/domain/repository/po/software_repository.go b/manager/backend/services/footstone/domain/repository/po/software_repository.go new file mode 100644 index 0000000000000000000000000000000000000000..e2836307fb62e4277aa9620e07287b25c7a9e0c2 --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/po/software_repository.go @@ -0,0 +1,80 @@ +package po + +import ( + "encoding/json" + "fmt" + "time" +) + +// SoftwareRepository stores management metadata only. RPM content and private +// keys never enter this table. +type SoftwareRepository struct { + ID string `json:"id" gorm:"column:id;type:char(36);primaryKey"` + ProjectID int64 `json:"project_id" gorm:"column:project_id;type:bigint;not null;index:idx_software_repositories_project;uniqueIndex:uniq_software_repository_scope;index:idx_software_repositories_purpose,priority:1"` + SystemProduct string `json:"system_product" gorm:"column:system_product;type:varchar(128);not null;uniqueIndex:uniq_software_repository_scope"` + ProductVersion string `json:"product_version" gorm:"column:product_version;type:varchar(64);not null;uniqueIndex:uniq_software_repository_scope"` + Name string `json:"name" gorm:"column:name;type:varchar(128);not null;uniqueIndex:uniq_software_repository_scope"` + Description string `json:"description" gorm:"column:description;type:varchar(1024);not null;default:''"` + Architecture string `json:"architecture" gorm:"column:architecture;type:varchar(32);not null;uniqueIndex:uniq_software_repository_scope;index:idx_software_repositories_purpose,priority:3"` + Type string `json:"type" gorm:"column:repository_type;type:varchar(32);not null"` + UpstreamURL string `json:"upstream_url" gorm:"column:upstream_url;type:varchar(2048);not null;default:''"` + StorageLocation string `json:"storage_location" gorm:"column:storage_location;type:varchar(512);not null"` + PublishKey string `json:"publish_key" gorm:"column:publish_key;type:varchar(64);not null"` + AllowUpload bool `json:"allow_upload" gorm:"column:allow_upload;type:tinyint(1);not null;default:0"` + IncludePackagesJSON string `json:"-" gorm:"column:include_packages;type:json;not null"` + ExcludePackagesJSON string `json:"-" gorm:"column:exclude_packages;type:json;not null"` + GPGPolicy string `json:"gpg_policy" gorm:"column:gpg_policy;type:varchar(32);not null"` + Enabled bool `json:"enabled" gorm:"column:enabled;type:tinyint(1);not null;default:1;index:idx_software_repositories_purpose,priority:4"` + Status string `json:"status" gorm:"column:status;type:varchar(32);not null"` + ActiveRevisionID string `json:"active_revision_id" gorm:"column:active_revision_id;type:char(36);not null;default:''"` + PrimaryStorageNodeID string `json:"primary_storage_node_id" gorm:"column:primary_storage_node_id;type:char(36);not null;default:'';index:idx_software_repositories_storage_node"` + PublishBaseURLSnapshot string `json:"publish_base_url_snapshot" gorm:"column:publish_base_url_snapshot;type:varchar(2048);not null;default:''"` + SyncMode string `json:"sync_mode" gorm:"column:sync_mode;type:varchar(16);not null;default:'MIRROR'"` + BandwidthLimitBytes int64 `json:"bandwidth_limit_bytes" gorm:"column:bandwidth_limit_bytes;type:bigint;not null;default:0"` + MaxParallelDownloads int `json:"max_parallel_downloads" gorm:"column:max_parallel_downloads;type:int;not null;default:3"` + MetadataSigningKeyRef string `json:"metadata_signing_key_ref" gorm:"column:metadata_signing_key_ref;type:varchar(128);not null;default:''"` + SystemManaged bool `json:"system_managed" gorm:"column:system_managed;type:tinyint(1);not null;default:0"` + Purpose string `json:"purpose" gorm:"column:purpose;type:varchar(32);not null;default:'';index:idx_software_repositories_purpose,priority:2"` + Creator string `json:"creator" gorm:"column:creator;type:varchar(256);not null"` + CreatedAt time.Time `json:"created_at" gorm:"<-:create;autoCreateTime;column:created_at;type:datetime(3);not null"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;column:updated_at;type:datetime(3);not null"` + + // List summaries are populated by repositoryrepo after the paged repository + // query. They are derived from immutable revisions and task history and are + // never persisted on software_repositories. + PackageCount int64 `json:"package_count" gorm:"-"` + LogicalBytes int64 `json:"logical_bytes" gorm:"-"` + RevisionCount int64 `json:"revision_count" gorm:"-"` + LastSyncAt *time.Time `json:"last_sync_at" gorm:"-"` + LastSyncStatus string `json:"last_sync_status" gorm:"-"` + MetadataSigningFingerprint string `json:"-" gorm:"-"` + MetadataSigningPublicKeyName string `json:"-" gorm:"-"` +} + +func (SoftwareRepository) TableName() string { + return "software_repositories" +} + +func (r *SoftwareRepository) SetPackageFilters(include, exclude []string) error { + includeJSON, err := json.Marshal(include) + if err != nil { + return fmt.Errorf("marshal include packages: %w", err) + } + excludeJSON, err := json.Marshal(exclude) + if err != nil { + return fmt.Errorf("marshal exclude packages: %w", err) + } + r.IncludePackagesJSON = string(includeJSON) + r.ExcludePackagesJSON = string(excludeJSON) + return nil +} + +func (r SoftwareRepository) PackageFilters() (include, exclude []string, err error) { + if err = json.Unmarshal([]byte(r.IncludePackagesJSON), &include); err != nil { + return nil, nil, fmt.Errorf("decode include packages: %w", err) + } + if err = json.Unmarshal([]byte(r.ExcludePackagesJSON), &exclude); err != nil { + return nil, nil, fmt.Errorf("decode exclude packages: %w", err) + } + return include, exclude, nil +} diff --git a/manager/backend/services/footstone/domain/repository/po/software_repository_test.go b/manager/backend/services/footstone/domain/repository/po/software_repository_test.go new file mode 100644 index 0000000000000000000000000000000000000000..a00e2fbfd008b297383b9009c16fd8296f1c0651 --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/po/software_repository_test.go @@ -0,0 +1,29 @@ +package po + +import ( + "reflect" + "testing" +) + +func TestSoftwareRepositoryPackageFiltersRoundTrip(t *testing.T) { + value := SoftwareRepository{} + wantInclude := []string{"kernel-*", "bash"} + wantExclude := []string{"*-debuginfo"} + if err := value.SetPackageFilters(wantInclude, wantExclude); err != nil { + t.Fatalf("SetPackageFilters() error = %v", err) + } + include, exclude, err := value.PackageFilters() + if err != nil { + t.Fatalf("PackageFilters() error = %v", err) + } + if !reflect.DeepEqual(include, wantInclude) || !reflect.DeepEqual(exclude, wantExclude) { + t.Fatalf("PackageFilters() = %v/%v, want %v/%v", include, exclude, wantInclude, wantExclude) + } +} + +func TestSoftwareRepositoryPackageFiltersRejectInvalidJSON(t *testing.T) { + value := SoftwareRepository{IncludePackagesJSON: "not-json", ExcludePackagesJSON: "[]"} + if _, _, err := value.PackageFilters(); err == nil { + t.Fatal("PackageFilters() error = nil, want invalid JSON error") + } +} diff --git a/manager/backend/services/footstone/domain/repository/repositoryairepo/repository.go b/manager/backend/services/footstone/domain/repository/repositoryairepo/repository.go new file mode 100644 index 0000000000000000000000000000000000000000..3124b32f9bed07b3d850f172d730686e2e366203 --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/repositoryairepo/repository.go @@ -0,0 +1,75 @@ +package repositoryairepo + +import ( + "errors" + + "gorm.io/gorm" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/baserepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +var ErrAnalysisNotFound = errors.New("repository OCAI analysis not found") + +type Repository interface { + SyncTables() error + Create(*po.RepositoryAIAnalysis, entity.Session) error + GetByID(string, entity.Session) (*po.RepositoryAIAnalysis, error) + GetBySessionID(string, entity.Session) (*po.RepositoryAIAnalysis, error) + UpdateFeedback(string, string, string, entity.Session) error +} + +type repository struct{ baserepo.DBBaseRepository } + +func New(client *database.DBClient) Repository { + return &repository{DBBaseRepository: baserepo.NewDBBaseRepository(client, []interface{}{ + new(po.RepositoryAIAnalysis), + })} +} + +func (r *repository) Create(value *po.RepositoryAIAnalysis, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + return tx.Create(value).Error +} + +func (r *repository) GetByID(id string, session entity.Session) (*po.RepositoryAIAnalysis, error) { + return r.get("id = ?", id, session) +} + +func (r *repository) GetBySessionID(id string, session entity.Session) (*po.RepositoryAIAnalysis, error) { + return r.get("session_id = ?", id, session) +} + +func (r *repository) get(query string, id string, session entity.Session) ( + value *po.RepositoryAIAnalysis, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + value = new(po.RepositoryAIAnalysis) + if err = tx.Where(query, id).Take(value).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrAnalysisNotFound + } + return nil, err + } + return value, nil +} + +func (r *repository) UpdateFeedback(sessionID, actionTaskID, actionResult string, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + result := tx.Model(&po.RepositoryAIAnalysis{}).Where("session_id = ?", sessionID).Updates(map[string]interface{}{ + "status": "FEEDBACK_RECORDED", "action_task_id": actionTaskID, + "action_result": actionResult, "feedback_at": gorm.Expr("CURRENT_TIMESTAMP(3)"), + }) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return ErrAnalysisNotFound + } + return nil +} diff --git a/manager/backend/services/footstone/domain/repository/repositorycontentrepo/repository.go b/manager/backend/services/footstone/domain/repository/repositorycontentrepo/repository.go new file mode 100644 index 0000000000000000000000000000000000000000..841fd3ef1bd23985c2c0e9694ca7055456e2b675 --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/repositorycontentrepo/repository.go @@ -0,0 +1,406 @@ +package repositorycontentrepo + +import ( + "errors" + "fmt" + + "github.com/go-sql-driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/clause" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/baserepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +var ( + ErrUploadNotFound = errors.New("repository upload not found") + ErrRevisionNotFound = errors.New("repository revision not found") + ErrGPGKeyDuplicate = errors.New("repository GPG key is already registered") + ErrGPGKeyNotFound = errors.New("repository GPG key not found") +) + +type Repository interface { + SyncTables() error + CreateUpload(*po.RepositoryUpload, entity.Session) error + GetUpload(string, bool, entity.Session) (*po.RepositoryUpload, error) + FindResumableUpload(string, string, string, entity.Session) (*po.RepositoryUpload, error) + UpdateUpload(*po.RepositoryUpload, entity.Session) error + PutUploadChunk(*po.RepositoryUploadChunk, entity.Session) (*po.RepositoryUploadChunk, bool, error) + GetUploadChunk(string, int, entity.Session) (*po.RepositoryUploadChunk, error) + ListUploadChunks(string, entity.Session) ([]po.RepositoryUploadChunk, error) + CreateRevision(*po.RepositoryRevision, []po.RepositoryPackage, entity.Session) error + LatestReadyRevision(string, entity.Session) (*po.RepositoryRevision, error) + GetRevision(string, string, entity.Session) (*po.RepositoryRevision, error) + RelocateRevision(string, string, string, string, string, entity.Session) error + PackagesByRevision(string, entity.Session) ([]po.RepositoryPackage, error) + HasPackagesByRevision(string, entity.Session) (bool, error) + ListPackages(request.ListRepositoryPackagesRequest, entity.Session) ([]po.RepositoryPackage, request.PageResponse, error) + CreateQuarantine(*po.RepositoryQuarantineItem, entity.Session) error + QuarantineByTask(string, entity.Session) ([]po.RepositoryQuarantineItem, error) + ListQuarantine(request.ListRepositoryQuarantineRequest, entity.Session) ([]po.RepositoryQuarantineItem, request.PageResponse, error) + CreateGPGKey(*po.RepositoryGPGKey, entity.Session) error + ListGPGKeys(string, entity.Session) ([]po.RepositoryGPGKey, error) + GetGPGKey(string, string, entity.Session) (*po.RepositoryGPGKey, error) + DeleteGPGKey(string, entity.Session) error + ReplaceMetadataSigningKeys(string, []po.RepositoryMetadataSigningKey, entity.Session) error + ListMetadataSigningKeys(string, entity.Session) ([]po.RepositoryMetadataSigningKey, error) + GetMetadataSigningKey(string, string, entity.Session) (*po.RepositoryMetadataSigningKey, error) +} + +type repository struct{ baserepo.DBBaseRepository } + +func New(client *database.DBClient) Repository { + return &repository{DBBaseRepository: baserepo.NewDBBaseRepository(client, []interface{}{ + new(po.RepositoryUpload), new(po.RepositoryUploadChunk), new(po.RepositoryRevision), new(po.RepositoryPackage), + new(po.RepositoryQuarantineItem), new(po.RepositoryGPGKey), + new(po.RepositoryMetadataSigningKey), + })} +} + +func (r *repository) ReplaceMetadataSigningKeys(nodeID string, values []po.RepositoryMetadataSigningKey, + session entity.Session, +) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + if err = tx.Where("storage_node_id = ?", nodeID).Delete(&po.RepositoryMetadataSigningKey{}).Error; err != nil { + return err + } + if len(values) == 0 { + return nil + } + return tx.CreateInBatches(values, 100).Error +} + +func (r *repository) ListMetadataSigningKeys(nodeID string, session entity.Session) ( + values []po.RepositoryMetadataSigningKey, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + err = tx.Where("storage_node_id = ?", nodeID).Order("key_ref ASC").Find(&values).Error + return values, err +} + +func (r *repository) GetMetadataSigningKey(nodeID, keyRef string, session entity.Session) ( + value *po.RepositoryMetadataSigningKey, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + value = new(po.RepositoryMetadataSigningKey) + if err = tx.Where("storage_node_id = ? AND key_ref = ?", nodeID, keyRef).Take(value).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + return value, nil +} + +func (r *repository) FindResumableUpload(repositoryID, requestedBy, fingerprint string, session entity.Session) ( + value *po.RepositoryUpload, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + value = new(po.RepositoryUpload) + err = tx.Where("repository_id = ? AND requested_by = ? AND client_fingerprint = ? AND status IN ?", + repositoryID, requestedBy, fingerprint, []string{"RECEIVING", "PAUSED"}). + Order("created_at DESC").Take(value).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return value, err +} + +func (r *repository) CreateUpload(value *po.RepositoryUpload, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + if err = tx.Create(value).Error; err != nil { + return normalizeWriteError("create repository upload", err) + } + return nil +} + +func (r *repository) GetUpload(id string, forUpdate bool, session entity.Session) ( + value *po.RepositoryUpload, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + if forUpdate { + tx = tx.Clauses(clause.Locking{Strength: "UPDATE"}) + } + value = new(po.RepositoryUpload) + if err = tx.Where("id = ?", id).Take(value).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrUploadNotFound + } + return nil, err + } + return value, nil +} + +func (r *repository) UpdateUpload(value *po.RepositoryUpload, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + if err = tx.Save(value).Error; err != nil { + return fmt.Errorf("update repository upload: %w", err) + } + return nil +} + +func (r *repository) PutUploadChunk(value *po.RepositoryUploadChunk, session entity.Session) ( + stored *po.RepositoryUploadChunk, created bool, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + result := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(value) + if result.Error != nil { + return nil, false, normalizeWriteError("create repository upload chunk", result.Error) + } + created = result.RowsAffected == 1 + stored = new(po.RepositoryUploadChunk) + if err = tx.Where("upload_id = ? AND chunk_index = ?", value.UploadID, value.ChunkIndex). + Take(stored).Error; err != nil { + return nil, false, err + } + return stored, created, nil +} + +func (r *repository) GetUploadChunk(uploadID string, chunkIndex int, session entity.Session) ( + value *po.RepositoryUploadChunk, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + value = new(po.RepositoryUploadChunk) + if err = tx.Where("upload_id = ? AND chunk_index = ?", uploadID, chunkIndex).Take(value).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + return value, nil +} + +func (r *repository) ListUploadChunks(uploadID string, session entity.Session) ( + values []po.RepositoryUploadChunk, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + err = tx.Where("upload_id = ?", uploadID).Order("chunk_index ASC").Find(&values).Error + return values, err +} + +func (r *repository) CreateRevision(value *po.RepositoryRevision, packages []po.RepositoryPackage, + session entity.Session, +) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + if err = tx.Create(value).Error; err != nil { + return normalizeWriteError("create repository revision", err) + } + if len(packages) > 0 { + if err = tx.CreateInBatches(packages, 100).Error; err != nil { + return normalizeWriteError("create repository packages", err) + } + } + return nil +} + +func (r *repository) LatestReadyRevision(repositoryID string, session entity.Session) ( + value *po.RepositoryRevision, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + value = new(po.RepositoryRevision) + err = tx.Where("repository_id = ? AND status IN ?", repositoryID, + []string{request.RepositoryRevisionStatusReady, request.RepositoryRevisionStatusPublished}). + Order("created_at DESC, id DESC").Take(value).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return value, err +} + +func (r *repository) GetRevision(repositoryID, revisionID string, session entity.Session) ( + value *po.RepositoryRevision, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + value = new(po.RepositoryRevision) + if err = tx.Where("repository_id = ? AND id = ?", repositoryID, revisionID).Take(value).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrRevisionNotFound + } + return nil, err + } + return value, nil +} + +// RelocateRevision changes only the storage location of an existing immutable revision. +// expectedSourceNodeID is part of the update predicate so a stale migration cannot move +// a revision that has already been relocated by another task. +func (r *repository) RelocateRevision(repositoryID, revisionID, expectedSourceNodeID, + targetStorageNodeID, relativePath string, session entity.Session, +) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + result := tx.Model(&po.RepositoryRevision{}).Where( + "repository_id = ? AND id = ? AND storage_node_id = ?", + repositoryID, revisionID, expectedSourceNodeID, + ).Updates(map[string]interface{}{"storage_node_id": targetStorageNodeID, "relative_path": relativePath}) + if result.Error != nil { + return result.Error + } + if result.RowsAffected > 0 { + return nil + } + stored := new(po.RepositoryRevision) + if err = tx.Where("repository_id = ? AND id = ?", repositoryID, revisionID).Take(stored).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrRevisionNotFound + } + return err + } + if stored.StorageNodeID == targetStorageNodeID && stored.RelativePath == relativePath { + return nil + } + return errors.New("repository revision storage location changed during migration") +} + +func (r *repository) PackagesByRevision(revisionID string, session entity.Session) ( + values []po.RepositoryPackage, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + err = tx.Where("revision_id = ?", revisionID).Order("nevra ASC").Find(&values).Error + return values, err +} + +func (r *repository) HasPackagesByRevision(revisionID string, session entity.Session) (exists bool, err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + var count int64 + err = tx.Model(&po.RepositoryPackage{}).Where("revision_id = ?", revisionID).Limit(1).Count(&count).Error + return count > 0, err +} + +func (r *repository) ListPackages(filter request.ListRepositoryPackagesRequest, session entity.Session) ( + values []po.RepositoryPackage, page request.PageResponse, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + tx = tx.Model(&po.RepositoryPackage{}).Where("repository_id = ? AND revision_id = ?", + filter.RepositoryID, filter.RevisionID) + if filter.Name != "" { + tx = tx.Where("name LIKE ?", "%"+filter.Name+"%") + } + if filter.Architecture != "" { + tx = tx.Where("architecture = ?", filter.Architecture) + } + var count int64 + if err = tx.Count(&count).Error; err != nil { + return nil, page, err + } + limit, offset := filter.PageRequest.Limit() + err = tx.Order("name ASC, epoch ASC, version ASC, package_release ASC, architecture ASC"). + Offset(offset).Limit(limit).Find(&values).Error + return values, filter.PageRequest.Response(count), err +} + +func (r *repository) CreateQuarantine(value *po.RepositoryQuarantineItem, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + if err = tx.Create(value).Error; err != nil { + return normalizeWriteError("create repository quarantine item", err) + } + return nil +} + +func (r *repository) QuarantineByTask(taskID string, session entity.Session) ( + values []po.RepositoryQuarantineItem, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + err = tx.Where("task_id = ?", taskID).Order("relative_path ASC").Find(&values).Error + return values, err +} + +func (r *repository) ListQuarantine(filter request.ListRepositoryQuarantineRequest, session entity.Session) ( + values []po.RepositoryQuarantineItem, page request.PageResponse, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + tx = tx.Model(&po.RepositoryQuarantineItem{}).Where("repository_id = ?", filter.RepositoryID) + if filter.TaskID != "" { + tx = tx.Where("task_id = ?", filter.TaskID) + } + if filter.ReasonCode != "" { + tx = tx.Where("reason_code = ?", filter.ReasonCode) + } + var count int64 + if err = tx.Count(&count).Error; err != nil { + return nil, page, err + } + limit, offset := filter.PageRequest.Limit() + err = tx.Order("created_at DESC, id DESC").Offset(offset).Limit(limit).Find(&values).Error + return values, filter.PageRequest.Response(count), err +} + +func (r *repository) CreateGPGKey(value *po.RepositoryGPGKey, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + if err = tx.Create(value).Error; err != nil { + var mysqlErr *mysql.MySQLError + if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 { + return ErrGPGKeyDuplicate + } + return fmt.Errorf("create repository GPG key: %w", err) + } + return nil +} + +func (r *repository) ListGPGKeys(repositoryID string, session entity.Session) ( + values []po.RepositoryGPGKey, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + err = tx.Where("repository_id = ?", repositoryID).Order("created_at ASC, fingerprint ASC").Find(&values).Error + return values, err +} + +func (r *repository) GetGPGKey(repositoryID, id string, session entity.Session) ( + value *po.RepositoryGPGKey, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + value = new(po.RepositoryGPGKey) + if err = tx.Where("repository_id = ? AND id = ?", repositoryID, id).Take(value).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrGPGKeyNotFound + } + return nil, err + } + return value, nil +} + +func (r *repository) DeleteGPGKey(id string, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + result := tx.Where("id = ?", id).Delete(&po.RepositoryGPGKey{}) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return ErrGPGKeyNotFound + } + return nil +} + +func normalizeWriteError(operation string, err error) error { + var mysqlErr *mysql.MySQLError + if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 { + return fmt.Errorf("%s: duplicate value", operation) + } + return fmt.Errorf("%s: %w", operation, err) +} diff --git a/manager/backend/services/footstone/domain/repository/repositorydistributionrepo/repository.go b/manager/backend/services/footstone/domain/repository/repositorydistributionrepo/repository.go new file mode 100644 index 0000000000000000000000000000000000000000..d77e9500ad87ebe7c15eb50d5cc3b94a9f56c9c6 --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/repositorydistributionrepo/repository.go @@ -0,0 +1,148 @@ +package repositorydistributionrepo + +import ( + "errors" + + "gorm.io/gorm" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/baserepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +var ErrDistributionNotFound = errors.New("repository distribution not found") + +type Repository interface { + SyncTables() error + Create(*po.RepositoryDistribution, []po.RepositoryDistributionTarget, entity.Session) error + Get(string, entity.Session) (*po.RepositoryDistribution, error) + GetByTaskID(string, entity.Session) (*po.RepositoryDistribution, error) + List(request.ListRepositoryDistributionsRequest, entity.Session) ([]po.RepositoryDistribution, request.PageResponse, error) + Start(string, string, entity.Session) error + SetTMS(string, string, string, entity.Session) error + Complete(*po.RepositoryDistribution, []po.RepositoryDistributionTarget, entity.Session) error +} + +type repository struct{ baserepo.DBBaseRepository } + +func New(client *database.DBClient) Repository { + return &repository{DBBaseRepository: baserepo.NewDBBaseRepository(client, []interface{}{ + new(po.RepositoryDistribution), new(po.RepositoryDistributionTarget), + })} +} + +func (r *repository) SyncTables() error { return r.DBBaseRepository.SyncTables() } + +func (r *repository) Create(value *po.RepositoryDistribution, targets []po.RepositoryDistributionTarget, + session entity.Session, +) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + if err = tx.Create(value).Error; err != nil { + return err + } + if len(targets) > 0 { + err = tx.Create(&targets).Error + } + return err +} + +func (r *repository) Get(id string, session entity.Session) (*po.RepositoryDistribution, error) { + return r.get("id = ?", id, session) +} + +func (r *repository) GetByTaskID(taskID string, session entity.Session) (*po.RepositoryDistribution, error) { + return r.get("repository_task_id = ?", taskID, session) +} + +func (r *repository) get(query string, value interface{}, session entity.Session) ( + result *po.RepositoryDistribution, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + result = new(po.RepositoryDistribution) + if err = tx.Where(query, value).Order("created_at DESC").Take(result).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrDistributionNotFound + } + return nil, err + } + err = tx.Where("distribution_id = ?", result.ID).Order("target_ip ASC").Find(&result.Targets).Error + return result, err +} + +func (r *repository) List(filter request.ListRepositoryDistributionsRequest, session entity.Session) ( + values []po.RepositoryDistribution, page request.PageResponse, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + tx = tx.Model(&po.RepositoryDistribution{}).Where("repository_id = ?", filter.RepositoryID) + var count int64 + if err = tx.Count(&count).Error; err != nil { + return nil, page, err + } + limit, offset := filter.PageRequest.Limit() + err = tx.Order("created_at DESC, id DESC").Offset(offset).Limit(limit).Find(&values).Error + if err != nil || len(values) == 0 { + return values, filter.PageRequest.Response(count), err + } + ids := make([]string, 0, len(values)) + for i := range values { + ids = append(ids, values[i].ID) + } + var targets []po.RepositoryDistributionTarget + targetTx := tx.Session(&gorm.Session{NewDB: true}).Model(&po.RepositoryDistributionTarget{}) + if err = targetTx.Where("distribution_id IN ?", ids).Order("target_ip ASC").Find(&targets).Error; err != nil { + return nil, page, err + } + byID := make(map[string][]po.RepositoryDistributionTarget, len(values)) + for _, target := range targets { + byID[target.DistributionID] = append(byID[target.DistributionID], target) + } + for i := range values { + values[i].Targets = byID[values[i].ID] + } + return values, filter.PageRequest.Response(count), nil +} + +func (r *repository) Start(id, taskID string, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + now := tx.NowFunc() + if err = tx.Model(&po.RepositoryDistribution{}).Where("id = ?", id).Updates(map[string]interface{}{ + "repository_task_id": taskID, "status": request.RepositoryDistributionStatusRunning, + "started_at": now, "finished_at": nil, "error_code": "", "error_message": "", + }).Error; err != nil { + return err + } + return tx.Model(&po.RepositoryDistributionTarget{}).Where("distribution_id = ?", id). + Updates(map[string]interface{}{"status": request.RepositoryDistributionTargetPending, + "attempt_count": 0, "exit_code": 0, "output": "", "output_ref": "", + "output_truncated": false, "error_code": "", "error_message": "", + "started_at": nil, "finished_at": nil}).Error +} + +func (r *repository) SetTMS(id, taskID, artifactMD5 string, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + return tx.Model(&po.RepositoryDistribution{}).Where("id = ?", id). + Updates(map[string]interface{}{"tms_task_id": taskID, "artifact_md5": artifactMD5}).Error +} + +func (r *repository) Complete(value *po.RepositoryDistribution, targets []po.RepositoryDistributionTarget, + session entity.Session, +) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + if err = tx.Save(value).Error; err != nil { + return err + } + for i := range targets { + if err = tx.Save(&targets[i]).Error; err != nil { + return err + } + } + return nil +} diff --git a/manager/backend/services/footstone/domain/repository/repositorydistributionrepo/repository_test.go b/manager/backend/services/footstone/domain/repository/repositorydistributionrepo/repository_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d519b68c0d170e07aa59bae860493af6cb1d3639 --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/repositorydistributionrepo/repository_test.go @@ -0,0 +1,64 @@ +package repositorydistributionrepo + +import ( + "database/sql" + "regexp" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/require" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/baserepo" +) + +type distributionTestSessionFactory struct{ db *gorm.DB } + +func (f distributionTestSessionFactory) NewSession() entity.Session { return f.db.Begin() } +func (f distributionTestSessionFactory) DB() entity.Session { return f.db } + +func newMockDistributionRepository(t *testing.T) (*repository, sqlmock.Sqlmock, *gorm.DB, *sql.DB) { + t.Helper() + sqlDB, mock, err := sqlmock.New() + require.NoError(t, err) + db, err := gorm.Open(mysql.New(mysql.Config{ + Conn: sqlDB, SkipInitializeWithVersion: true, + }), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) + require.NoError(t, err) + return &repository{DBBaseRepository: baserepo.NewDBBaseRepository( + distributionTestSessionFactory{db: db}, nil, + )}, mock, db, sqlDB +} + +func TestListLoadsTargetsFromTargetTable(t *testing.T) { + repo, mock, db, sqlDB := newMockDistributionRepository(t) + + mock.ExpectQuery(regexp.QuoteMeta( + "SELECT count(*) FROM `repository_distributions` WHERE repository_id = ?", + )).WithArgs("repo-1").WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) + mock.ExpectQuery("SELECT \\* FROM `repository_distributions`.*repository_id = \\?.*ORDER BY created_at DESC, id DESC"). + WithArgs("repo-1"). + WillReturnRows(sqlmock.NewRows([]string{"id", "repository_id"}).AddRow("dist-1", "repo-1")) + mock.ExpectQuery(regexp.QuoteMeta( + "SELECT * FROM `repository_distribution_targets` WHERE distribution_id IN (?) ORDER BY target_ip ASC", + )).WithArgs("dist-1").WillReturnRows(sqlmock.NewRows( + []string{"id", "distribution_id", "target_ip"}, + ).AddRow("target-1", "dist-1", "192.0.2.10")) + + values, page, err := repo.List(request.ListRepositoryDistributionsRequest{ + RepositoryID: "repo-1", + PageRequest: request.PageRequest{PageNumber: 1, PageSize: 20}, + }, db) + require.NoError(t, err) + require.Equal(t, int64(1), page.Count) + require.Len(t, values, 1) + require.Len(t, values[0].Targets, 1) + require.Equal(t, "192.0.2.10", values[0].Targets[0].TargetIP) + mock.ExpectClose() + require.NoError(t, sqlDB.Close()) + require.NoError(t, mock.ExpectationsWereMet()) +} diff --git a/manager/backend/services/footstone/domain/repository/repositoryinventoryrepo/repository.go b/manager/backend/services/footstone/domain/repository/repositoryinventoryrepo/repository.go new file mode 100644 index 0000000000000000000000000000000000000000..f74af1f215d2661bf0c499cb4a859e9c32df2160 --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/repositoryinventoryrepo/repository.go @@ -0,0 +1,45 @@ +package repositoryinventoryrepo + +import ( + "errors" + + "gorm.io/gorm" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/baserepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +var ErrImportJobNotFound = errors.New("repository inventory import job not found") + +type Repository interface { + SyncTables() error + Create(*po.RepositoryInventoryImportJob, entity.Session) error + GetByPreviewID(string, entity.Session) (*po.RepositoryInventoryImportJob, error) +} + +type repository struct{ baserepo.DBBaseRepository } + +func New(client *database.DBClient) Repository { + return &repository{DBBaseRepository: baserepo.NewDBBaseRepository(client, + []interface{}{new(po.RepositoryInventoryImportJob)})} +} + +func (r *repository) Create(value *po.RepositoryInventoryImportJob, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + return tx.Create(value).Error +} + +func (r *repository) GetByPreviewID(previewID string, session entity.Session) ( + value *po.RepositoryInventoryImportJob, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + value = new(po.RepositoryInventoryImportJob) + if err = tx.Where("preview_id = ?", previewID).Take(value).Error; errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrImportJobNotFound + } + return value, err +} diff --git a/manager/backend/services/footstone/domain/repository/repositorylockrepo/repository.go b/manager/backend/services/footstone/domain/repository/repositorylockrepo/repository.go new file mode 100644 index 0000000000000000000000000000000000000000..02c291bffc5abd65910ad4c6ad60f50b32733927 --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/repositorylockrepo/repository.go @@ -0,0 +1,156 @@ +package repositorylockrepo + +import ( + "errors" + "time" + + "github.com/go-sql-driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/clause" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/baserepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +var ( + ErrLockHeld = errors.New("repository mutation lock is held") + ErrLockLost = errors.New("repository mutation lock ownership is lost") +) + +type Repository interface { + SyncTables() error + TryAcquire(repositoryID, taskID, workerID string, now, leaseUntil time.Time, session entity.Session) (uint64, error) + Renew(repositoryID, taskID, workerID string, fencingToken uint64, now, leaseUntil time.Time, session entity.Session) error + Release(repositoryID, taskID, workerID string, fencingToken uint64, now time.Time, session entity.Session) error + Get(string, entity.Session) (*po.RepositoryMutationLock, error) + GetForUpdate(string, entity.Session) (*po.RepositoryMutationLock, error) +} + +type repository struct { + baserepo.DBBaseRepository +} + +func New(client *database.DBClient) Repository { + return &repository{ + DBBaseRepository: baserepo.NewDBBaseRepository(client, []interface{}{new(po.RepositoryMutationLock)}), + } +} + +func (r *repository) TryAcquire(repositoryID, taskID, workerID string, now, leaseUntil time.Time, + session entity.Session, +) (token uint64, err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + value := new(po.RepositoryMutationLock) + err = tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("repository_id = ?", repositoryID).Take(value).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + value = &po.RepositoryMutationLock{ + RepositoryID: repositoryID, + OwnerTaskID: taskID, + WorkerID: workerID, + FencingToken: 1, + LeaseExpiresAt: timePointer(leaseUntil), + LastHeartbeatAt: timePointer(now), + AcquiredAt: timePointer(now), + } + if err = tx.Create(value).Error; err != nil { + var mysqlErr *mysql.MySQLError + if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 { + return 0, ErrLockHeld + } + return 0, err + } + return value.FencingToken, nil + } + if err != nil { + return 0, err + } + if value.OwnerTaskID != "" && value.LeaseExpiresAt != nil && value.LeaseExpiresAt.After(now) { + return 0, ErrLockHeld + } + value.OwnerTaskID = taskID + value.WorkerID = workerID + value.FencingToken++ + value.LeaseExpiresAt = timePointer(leaseUntil) + value.LastHeartbeatAt = timePointer(now) + value.AcquiredAt = timePointer(now) + value.ReleasedAt = nil + if err = tx.Save(value).Error; err != nil { + return 0, err + } + return value.FencingToken, nil +} + +func (r *repository) Renew(repositoryID, taskID, workerID string, fencingToken uint64, now, leaseUntil time.Time, + session entity.Session, +) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + result := tx.Model(&po.RepositoryMutationLock{}). + Where(`repository_id = ? AND owner_task_id = ? AND worker_id = ? AND fencing_token = ? + AND lease_expires_at IS NOT NULL AND lease_expires_at > ?`, + repositoryID, taskID, workerID, fencingToken, now). + Updates(map[string]interface{}{"lease_expires_at": leaseUntil, "last_heartbeat_at": now}) + if result.Error != nil { + return result.Error + } + if result.RowsAffected != 1 { + return ErrLockLost + } + return nil +} + +func (r *repository) Release(repositoryID, taskID, workerID string, fencingToken uint64, now time.Time, + session entity.Session, +) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + result := tx.Model(&po.RepositoryMutationLock{}). + Where("repository_id = ? AND owner_task_id = ? AND worker_id = ? AND fencing_token = ?", + repositoryID, taskID, workerID, fencingToken). + Updates(map[string]interface{}{ + "owner_task_id": "", + "worker_id": "", + "lease_expires_at": nil, + "last_heartbeat_at": now, + "released_at": now, + }) + if result.Error != nil { + return result.Error + } + if result.RowsAffected != 1 { + return ErrLockLost + } + return nil +} + +func (r *repository) Get(repositoryID string, session entity.Session) ( + value *po.RepositoryMutationLock, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + value = new(po.RepositoryMutationLock) + if err = tx.Where("repository_id = ?", repositoryID).Take(value).Error; err != nil { + return nil, err + } + return value, nil +} + +func (r *repository) GetForUpdate(repositoryID string, session entity.Session) ( + value *po.RepositoryMutationLock, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + value = new(po.RepositoryMutationLock) + if err = tx.Clauses(clause.Locking{Strength: "UPDATE"}). + Where("repository_id = ?", repositoryID).Take(value).Error; err != nil { + return nil, err + } + return value, nil +} + +func timePointer(value time.Time) *time.Time { + return &value +} diff --git a/manager/backend/services/footstone/domain/repository/repositorypackageinstallrepo/repository.go b/manager/backend/services/footstone/domain/repository/repositorypackageinstallrepo/repository.go new file mode 100644 index 0000000000000000000000000000000000000000..44edd274145e7dca7ced46efced18ed20e83f6f9 --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/repositorypackageinstallrepo/repository.go @@ -0,0 +1,110 @@ +package repositorypackageinstallrepo + +import ( + "errors" + + "gorm.io/gorm" + "gorm.io/gorm/clause" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/baserepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +var ( + ErrPreviewNotFound = errors.New("repository operation preview not found") + ErrInstallNotFound = errors.New("repository package install not found") +) + +type Repository interface { + SyncTables() error + CreatePreview(*po.RepositoryOperationPreview, entity.Session) error + GetPreview(string, bool, entity.Session) (*po.RepositoryOperationPreview, error) + UpdatePreview(*po.RepositoryOperationPreview, entity.Session) error + CreateInstall(*po.RepositoryPackageInstall, entity.Session) error + GetInstall(string, entity.Session) (*po.RepositoryPackageInstall, error) + GetInstallByPreview(string, entity.Session) (*po.RepositoryPackageInstall, error) + UpdateInstall(*po.RepositoryPackageInstall, entity.Session) error + ListActiveInstalls(entity.Session) ([]po.RepositoryPackageInstall, error) +} + +type repository struct{ baserepo.DBBaseRepository } + +func New(client *database.DBClient) Repository { + return &repository{DBBaseRepository: baserepo.NewDBBaseRepository(client, []interface{}{ + new(po.RepositoryOperationPreview), new(po.RepositoryPackageInstall), + })} +} + +func (r *repository) CreatePreview(value *po.RepositoryOperationPreview, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + return tx.Create(value).Error +} + +func (r *repository) GetPreview(id string, forUpdate bool, session entity.Session) ( + value *po.RepositoryOperationPreview, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + if forUpdate { + tx = tx.Clauses(clause.Locking{Strength: "UPDATE"}) + } + value = new(po.RepositoryOperationPreview) + if err = tx.Where("id = ?", id).Take(value).Error; errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrPreviewNotFound + } + return value, err +} + +func (r *repository) UpdatePreview(value *po.RepositoryOperationPreview, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + return tx.Save(value).Error +} + +func (r *repository) CreateInstall(value *po.RepositoryPackageInstall, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + return tx.Create(value).Error +} + +func (r *repository) GetInstall(id string, session entity.Session) ( + value *po.RepositoryPackageInstall, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + value = new(po.RepositoryPackageInstall) + if err = tx.Where("id = ?", id).Take(value).Error; errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrInstallNotFound + } + return value, err +} + +func (r *repository) GetInstallByPreview(previewID string, session entity.Session) ( + value *po.RepositoryPackageInstall, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + value = new(po.RepositoryPackageInstall) + if err = tx.Where("preview_id = ?", previewID).Take(value).Error; errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrInstallNotFound + } + return value, err +} + +func (r *repository) UpdateInstall(value *po.RepositoryPackageInstall, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + return tx.Save(value).Error +} + +func (r *repository) ListActiveInstalls(session entity.Session) ( + values []po.RepositoryPackageInstall, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + err = tx.Where("status IN ?", []string{"PENDING", "RUNNING"}).Order("created_at ASC").Find(&values).Error + return values, err +} diff --git a/manager/backend/services/footstone/domain/repository/repositoryrepo/repository.go b/manager/backend/services/footstone/domain/repository/repositoryrepo/repository.go new file mode 100644 index 0000000000000000000000000000000000000000..e22ae38d5e263c9c67e851c56d5c99e9e7e9ab69 --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/repositoryrepo/repository.go @@ -0,0 +1,273 @@ +package repositoryrepo + +import ( + "errors" + "fmt" + + "github.com/go-sql-driver/mysql" + "gorm.io/gorm" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/baserepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +var ( + ErrRepositoryNotFound = errors.New("software repository not found") + ErrRepositoryDuplicate = errors.New("software repository already exists in this product/version scope") +) + +type Repository interface { + SyncTables() error + Create(*po.SoftwareRepository, entity.Session) error + Update(*po.SoftwareRepository, entity.Session) error + GetByID(string, entity.Session) (*po.SoftwareRepository, error) + List(request.ListSoftwareRepositoriesRequest, entity.Session) ([]po.SoftwareRepository, request.PageResponse, error) + Delete(string, entity.Session) error + UpdateStatus(string, string, entity.Session) error + UpdateSystemPurpose(string, bool, string, entity.Session) error + ActivateRevision(string, string, string, entity.Session) error +} + +type repository struct { + baserepo.DBBaseRepository +} + +func New(client *database.DBClient) Repository { + return &repository{ + DBBaseRepository: baserepo.NewDBBaseRepository(client, []interface{}{new(po.SoftwareRepository)}), + } +} + +func (r *repository) Create(value *po.SoftwareRepository, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + if err = tx.Create(value).Error; err != nil { + return normalizeWriteError(err) + } + return nil +} + +func (r *repository) Update(value *po.SoftwareRepository, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + result := tx.Model(&po.SoftwareRepository{}).Where("id = ?", value.ID).Updates(map[string]interface{}{ + "system_product": value.SystemProduct, + "product_version": value.ProductVersion, + "name": value.Name, + "description": value.Description, + "architecture": value.Architecture, + "repository_type": value.Type, + "upstream_url": value.UpstreamURL, + "allow_upload": value.AllowUpload, + "include_packages": value.IncludePackagesJSON, + "exclude_packages": value.ExcludePackagesJSON, + "gpg_policy": value.GPGPolicy, + "enabled": value.Enabled, + "primary_storage_node_id": value.PrimaryStorageNodeID, + "publish_base_url_snapshot": value.PublishBaseURLSnapshot, + "sync_mode": value.SyncMode, + "bandwidth_limit_bytes": value.BandwidthLimitBytes, + "max_parallel_downloads": value.MaxParallelDownloads, + "metadata_signing_key_ref": value.MetadataSigningKeyRef, + }) + if result.Error != nil { + return normalizeWriteError(result.Error) + } + return nil +} + +func (r *repository) GetByID(id string, session entity.Session) (_ *po.SoftwareRepository, err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + value := new(po.SoftwareRepository) + if err = tx.Where("id = ?", id).Take(value).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrRepositoryNotFound + } + return nil, err + } + values := []po.SoftwareRepository{*value} + if err = enrichRepositorySummaries(tx, values); err != nil { + return nil, err + } + *value = values[0] + return value, nil +} + +func (r *repository) List(filter request.ListSoftwareRepositoriesRequest, session entity.Session) ( + values []po.SoftwareRepository, page request.PageResponse, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + tx = tx.Model(&po.SoftwareRepository{}).Where("project_id = ?", filter.ProjectID) + if filter.SystemProduct != "" { + tx = tx.Where("system_product = ?", filter.SystemProduct) + } + if filter.ProductVersion != "" { + tx = tx.Where("product_version = ?", filter.ProductVersion) + } + if filter.Architecture != "" { + tx = tx.Where("architecture = ?", filter.Architecture) + } + if filter.Type != "" { + tx = tx.Where("repository_type = ?", filter.Type) + } + if filter.Keyword != "" { + keyword := "%" + filter.Keyword + "%" + tx = tx.Where("name LIKE ? OR description LIKE ?", keyword, keyword) + } + + var count int64 + if err = tx.Count(&count).Error; err != nil { + return nil, page, err + } + limit, offset := filter.PageRequest.Limit() + if err = tx.Order("system_product ASC, product_version ASC, name ASC, architecture ASC"). + Offset(offset).Limit(limit).Find(&values).Error; err != nil { + return nil, page, err + } + if err = enrichRepositorySummaries(tx, values); err != nil { + return nil, page, err + } + return values, filter.PageRequest.Response(count), nil +} + +func enrichRepositorySummaries(tx *gorm.DB, values []po.SoftwareRepository) error { + if len(values) == 0 { + return nil + } + repositoryIDs := make([]string, 0, len(values)) + revisionIDs := make([]string, 0, len(values)) + positions := make(map[string]int, len(values)) + lookup := tx.Session(&gorm.Session{NewDB: true}) + for index := range values { + repositoryIDs = append(repositoryIDs, values[index].ID) + positions[values[index].ID] = index + if values[index].ActiveRevisionID != "" { + revisionIDs = append(revisionIDs, values[index].ActiveRevisionID) + } + } + if len(revisionIDs) > 0 { + var revisions []po.RepositoryRevision + if err := lookup.Select("id", "repository_id", "package_count", "total_bytes"). + Where("id IN ?", revisionIDs).Find(&revisions).Error; err != nil { + return err + } + for _, revision := range revisions { + if index, ok := positions[revision.RepositoryID]; ok { + values[index].PackageCount = int64(revision.PackageCount) + values[index].LogicalBytes = revision.TotalBytes + } + } + } + type revisionCount struct { + RepositoryID string `gorm:"column:repository_id"` + Count int64 `gorm:"column:revision_count"` + } + var revisionCounts []revisionCount + if err := lookup.Model(&po.RepositoryRevision{}). + Select("repository_id, COUNT(*) AS revision_count"). + Where("repository_id IN ?", repositoryIDs). + Group("repository_id").Scan(&revisionCounts).Error; err != nil { + return err + } + for _, count := range revisionCounts { + if index, ok := positions[count.RepositoryID]; ok { + values[index].RevisionCount = count.Count + } + } + var syncTasks []po.RepositoryTask + if err := lookup.Where("repository_id IN ? AND task_type = ? AND status IN ?", repositoryIDs, + request.RepositoryTaskTypeSync, []string{request.RepositoryTaskStatusSuccess, + request.RepositoryTaskStatusFailed, request.RepositoryTaskStatusCanceled, + request.RepositoryTaskStatusPartialSuccess}). + Order("repository_id ASC, created_at DESC, id DESC").Find(&syncTasks).Error; err != nil { + return err + } + seen := make(map[string]struct{}, len(values)) + for index := range syncTasks { + task := &syncTasks[index] + if _, ok := seen[task.RepositoryID]; ok { + continue + } + position, ok := positions[task.RepositoryID] + if !ok { + continue + } + seen[task.RepositoryID] = struct{}{} + values[position].LastSyncAt = task.FinishedAt + values[position].LastSyncStatus = task.Status + } + return nil +} + +func (r *repository) Delete(id string, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + result := tx.Where("id = ?", id).Delete(&po.SoftwareRepository{}) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return ErrRepositoryNotFound + } + return nil +} + +func (r *repository) UpdateStatus(id, status string, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + result := tx.Model(&po.SoftwareRepository{}).Where("id = ?", id).Update("status", status) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return ErrRepositoryNotFound + } + return nil +} + +func (r *repository) UpdateSystemPurpose(id string, systemManaged bool, purpose string, + session entity.Session, +) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + result := tx.Model(&po.SoftwareRepository{}).Where("id = ?", id).Updates(map[string]interface{}{ + "system_managed": systemManaged, + "purpose": purpose, + }) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return ErrRepositoryNotFound + } + return nil +} + +func (r *repository) ActivateRevision(id, revisionID, status string, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + result := tx.Model(&po.SoftwareRepository{}).Where("id = ?", id).Updates(map[string]interface{}{ + "active_revision_id": revisionID, + "status": status, + }) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return ErrRepositoryNotFound + } + return nil +} + +func normalizeWriteError(err error) error { + var mysqlErr *mysql.MySQLError + if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 { + return ErrRepositoryDuplicate + } + return fmt.Errorf("write software repository: %w", err) +} diff --git a/manager/backend/services/footstone/domain/repository/repositoryrepo/repository_test.go b/manager/backend/services/footstone/domain/repository/repositoryrepo/repository_test.go new file mode 100644 index 0000000000000000000000000000000000000000..8c8fcb7fa7efcc0cda7e5b5e69b48b8ee29ddbac --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/repositoryrepo/repository_test.go @@ -0,0 +1,23 @@ +package repositoryrepo + +import ( + "errors" + "testing" + + "github.com/go-sql-driver/mysql" +) + +func TestNormalizeWriteErrorDuplicate(t *testing.T) { + err := normalizeWriteError(&mysql.MySQLError{Number: 1062, Message: "duplicate"}) + if !errors.Is(err, ErrRepositoryDuplicate) { + t.Fatalf("normalizeWriteError(1062) = %v", err) + } +} + +func TestNormalizeWriteErrorPreservesCause(t *testing.T) { + cause := errors.New("write failed") + err := normalizeWriteError(cause) + if !errors.Is(err, cause) { + t.Fatalf("normalizeWriteError() = %v, want wrapped cause", err) + } +} diff --git a/manager/backend/services/footstone/domain/repository/repositoryschedulerepo/repository.go b/manager/backend/services/footstone/domain/repository/repositoryschedulerepo/repository.go new file mode 100644 index 0000000000000000000000000000000000000000..773808af184aa203e66c8bf460310bbafc49af4e --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/repositoryschedulerepo/repository.go @@ -0,0 +1,137 @@ +package repositoryschedulerepo + +import ( + "errors" + "fmt" + "time" + + "github.com/go-sql-driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/clause" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/baserepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +var ( + ErrScheduleNotFound = errors.New("repository sync schedule not found") + ErrScheduleRunDuplicate = errors.New("repository schedule run already exists") +) + +type Repository interface { + SyncTables() error + CreateSchedule(*po.RepositorySyncSchedule, entity.Session) error + UpdateSchedule(*po.RepositorySyncSchedule, entity.Session) error + GetScheduleByRepositoryID(string, entity.Session) (*po.RepositorySyncSchedule, error) + GetScheduleForUpdate(string, entity.Session) (*po.RepositorySyncSchedule, error) + ListDue(time.Time, int, entity.Session) ([]po.RepositorySyncSchedule, error) + CreateRun(*po.RepositoryScheduleRun, entity.Session) error + GetRunByKey(string, entity.Session) (*po.RepositoryScheduleRun, error) + ListOpenRuns(int, entity.Session) ([]po.RepositoryScheduleRun, error) + UpdateRun(*po.RepositoryScheduleRun, entity.Session) error +} + +type repository struct{ baserepo.DBBaseRepository } + +func New(client *database.DBClient) Repository { + return &repository{DBBaseRepository: baserepo.NewDBBaseRepository(client, []interface{}{ + new(po.RepositorySyncSchedule), new(po.RepositoryScheduleRun), + })} +} + +func (r *repository) CreateSchedule(value *po.RepositorySyncSchedule, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + return tx.Create(value).Error +} + +func (r *repository) UpdateSchedule(value *po.RepositorySyncSchedule, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + return tx.Save(value).Error +} + +func (r *repository) GetScheduleByRepositoryID(repositoryID string, session entity.Session) ( + *po.RepositorySyncSchedule, error, +) { + return r.getSchedule(repositoryID, false, session) +} + +func (r *repository) GetScheduleForUpdate(repositoryID string, session entity.Session) ( + *po.RepositorySyncSchedule, error, +) { + return r.getSchedule(repositoryID, true, session) +} + +func (r *repository) getSchedule(repositoryID string, forUpdate bool, session entity.Session) ( + value *po.RepositorySyncSchedule, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + if forUpdate { + tx = tx.Clauses(clause.Locking{Strength: "UPDATE"}) + } + value = new(po.RepositorySyncSchedule) + if err = tx.Where("repository_id = ?", repositoryID).Take(value).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrScheduleNotFound + } + return nil, err + } + return value, nil +} + +func (r *repository) ListDue(now time.Time, limit int, session entity.Session) ( + values []po.RepositorySyncSchedule, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + err = tx.Where("status = ? AND next_run_at <= ?", request.RepositoryScheduleStatusEnabled, now). + Order("next_run_at ASC, id ASC").Limit(limit).Find(&values).Error + return values, err +} + +func (r *repository) CreateRun(value *po.RepositoryScheduleRun, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + if err = tx.Create(value).Error; err != nil { + var mysqlErr *mysql.MySQLError + if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 { + return ErrScheduleRunDuplicate + } + return fmt.Errorf("create repository schedule run: %w", err) + } + return nil +} + +func (r *repository) GetRunByKey(key string, session entity.Session) ( + value *po.RepositoryScheduleRun, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + value = new(po.RepositoryScheduleRun) + if err = tx.Clauses(clause.Locking{Strength: "UPDATE"}). + Where("schedule_run_key = ?", key).Take(value).Error; err != nil { + return nil, err + } + return value, nil +} + +func (r *repository) ListOpenRuns(limit int, session entity.Session) ( + values []po.RepositoryScheduleRun, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + err = tx.Where("status IN ?", []string{request.RepositoryScheduleRunPending, + request.RepositoryScheduleRunTaskCreated}).Order("created_at ASC, id ASC").Limit(limit).Find(&values).Error + return values, err +} + +func (r *repository) UpdateRun(value *po.RepositoryScheduleRun, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + return tx.Save(value).Error +} diff --git a/manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/lifecycle.go b/manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/lifecycle.go new file mode 100644 index 0000000000000000000000000000000000000000..6fa170abbb06283f85752a9c2f82b2ced51e3c87 --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/lifecycle.go @@ -0,0 +1,404 @@ +package repositorystoragenoderepo + +import ( + "errors" + "time" + + "gorm.io/gorm" + "gorm.io/gorm/clause" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/baserepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +var ( + ErrNodeEnrollmentNotFound = errors.New("repository node enrollment not found") + ErrNodeBootstrapNotFound = errors.New("repository node bootstrap token not found") + ErrNodeInstallSourceNotFound = errors.New("repository node install source not found") + ErrNodeAllowlistNotFound = errors.New("repository node registration allowlist not found") + ErrNodeCertificateNotFound = errors.New("repository node certificate not found") + ErrNodeArtifactNotFound = errors.New("repository node artifact not found") +) + +type LifecycleRepository interface { + SyncTables() error + CreateEnrollment(*po.RepositoryNodeEnrollment, entity.Session) error + UpdateEnrollment(*po.RepositoryNodeEnrollment, entity.Session) error + GetEnrollmentByIDForUpdate(string, entity.Session) (*po.RepositoryNodeEnrollment, error) + FindLatestEnrollmentByNode(string, entity.Session) (*po.RepositoryNodeEnrollment, error) + FindEnrollmentByAgent(uint64, entity.Session) (*po.RepositoryNodeEnrollment, error) + ListActiveInstallations(int, entity.Session) ([]po.RepositoryNodeEnrollment, error) + CreateBootstrapToken(*po.RepositoryNodeBootstrapToken, entity.Session) error + GetBootstrapTokenByHashForUpdate(string, entity.Session) (*po.RepositoryNodeBootstrapToken, error) + UpdateBootstrapToken(*po.RepositoryNodeBootstrapToken, entity.Session) error + InvalidateBootstrapTokens(string, time.Time, entity.Session) error + CreateInstallSource(*po.RepositoryNodeInstallSource, entity.Session) error + UpdateInstallSource(*po.RepositoryNodeInstallSource, entity.Session) error + DeleteInstallSource(string, entity.Session) error + GetInstallSource(int64, string, bool, entity.Session) (*po.RepositoryNodeInstallSource, error) + ListInstallSources(int64, entity.Session) ([]po.RepositoryNodeInstallSource, error) + CreateAllowlist(*po.RepositoryNodeRegistrationAllowlist, entity.Session) error + UpdateAllowlist(*po.RepositoryNodeRegistrationAllowlist, entity.Session) error + DeleteAllowlist(string, entity.Session) error + GetAllowlist(int64, uint64, bool, entity.Session) (*po.RepositoryNodeRegistrationAllowlist, error) + ListAllowlists(int64, entity.Session) ([]po.RepositoryNodeRegistrationAllowlist, error) + CreateCertificate(*po.RepositoryNodeCertificate, entity.Session) error + FindCertificateByAuthorizationID(string, entity.Session) (*po.RepositoryNodeCertificate, error) + SupersedeActiveCertificates(string, time.Time, entity.Session) error + FindUsableCertificate(string, string, time.Time, entity.Session) (*po.RepositoryNodeCertificate, error) + CountCertificates(string, entity.Session) (int64, error) + RevokeCertificates(string, time.Time, entity.Session) error + CreateArtifact(*po.RepositoryNodeArtifact, entity.Session) error + UpdateArtifact(*po.RepositoryNodeArtifact, entity.Session) error + GetArtifactByID(string, entity.Session) (*po.RepositoryNodeArtifact, error) + GetArtifact(string, string, string, entity.Session) (*po.RepositoryNodeArtifact, error) + ListArtifacts(string, entity.Session) ([]po.RepositoryNodeArtifact, error) + ListArtifactCandidates(int64, string, string, string, entity.Session) ([]po.RepositoryNodeArtifact, error) +} + +type lifecycleRepository struct{ baserepo.DBBaseRepository } + +func NewLifecycleRepository(client *database.DBClient) LifecycleRepository { + return &lifecycleRepository{DBBaseRepository: baserepo.NewDBBaseRepository(client, []interface{}{ + new(po.RepositoryNodeEnrollment), new(po.RepositoryNodeBootstrapToken), + new(po.RepositoryNodeInstallSource), new(po.RepositoryNodeRegistrationAllowlist), + new(po.RepositoryNodeCertificate), new(po.RepositoryNodeArtifact), + })} +} + +func (r *lifecycleRepository) CreateEnrollment(value *po.RepositoryNodeEnrollment, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + return tx.Create(value).Error +} + +func (r *lifecycleRepository) UpdateEnrollment(value *po.RepositoryNodeEnrollment, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + return tx.Save(value).Error +} + +func (r *lifecycleRepository) GetEnrollmentByIDForUpdate(id string, session entity.Session) ( + value *po.RepositoryNodeEnrollment, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + value = new(po.RepositoryNodeEnrollment) + err = tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", id).Take(value).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrNodeEnrollmentNotFound + } + return value, err +} + +func (r *lifecycleRepository) FindLatestEnrollmentByNode(nodeID string, session entity.Session) ( + value *po.RepositoryNodeEnrollment, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + value = new(po.RepositoryNodeEnrollment) + err = tx.Where("node_id = ?", nodeID).Order("generation DESC").Take(value).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrNodeEnrollmentNotFound + } + return value, err +} + +func (r *lifecycleRepository) FindEnrollmentByAgent(agentID uint64, session entity.Session) ( + value *po.RepositoryNodeEnrollment, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + value = new(po.RepositoryNodeEnrollment) + err = tx.Where("tms_agent_id = ?", agentID).Order("generation DESC").Take(value).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrNodeEnrollmentNotFound + } + return value, err +} + +func (r *lifecycleRepository) ListActiveInstallations(limit int, session entity.Session) ( + values []po.RepositoryNodeEnrollment, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + if limit <= 0 || limit > 200 { + limit = 100 + } + err = tx.Where("status IN ?", []string{"PENDING", "INSTALLING", "AUTHORIZED", "REGISTERING", "REGISTERED", "BOOTSTRAPPED"}). + Order("updated_at ASC").Limit(limit).Find(&values).Error + return values, err +} + +func (r *lifecycleRepository) CreateBootstrapToken(value *po.RepositoryNodeBootstrapToken, + session entity.Session, +) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + return tx.Create(value).Error +} + +func (r *lifecycleRepository) GetBootstrapTokenByHashForUpdate(hash string, session entity.Session) ( + value *po.RepositoryNodeBootstrapToken, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + value = new(po.RepositoryNodeBootstrapToken) + err = tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("token_hash = ?", hash).Take(value).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrNodeBootstrapNotFound + } + return value, err +} + +func (r *lifecycleRepository) UpdateBootstrapToken(value *po.RepositoryNodeBootstrapToken, + session entity.Session, +) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + return tx.Save(value).Error +} + +func (r *lifecycleRepository) InvalidateBootstrapTokens(enrollmentID string, at time.Time, + session entity.Session, +) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + return tx.Model(&po.RepositoryNodeBootstrapToken{}). + Where("enrollment_id = ? AND consumed_at IS NULL AND invalidated_at IS NULL", enrollmentID). + Update("invalidated_at", at).Error +} + +func (r *lifecycleRepository) CreateInstallSource(value *po.RepositoryNodeInstallSource, + session entity.Session, +) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + return tx.Create(value).Error +} + +func (r *lifecycleRepository) UpdateInstallSource(value *po.RepositoryNodeInstallSource, + session entity.Session, +) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + return tx.Save(value).Error +} + +func (r *lifecycleRepository) DeleteInstallSource(id string, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + result := tx.Where("id = ?", id).Delete(&po.RepositoryNodeInstallSource{}) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return ErrNodeInstallSourceNotFound + } + return nil +} + +func (r *lifecycleRepository) GetInstallSource(projectID int64, architecture string, forUpdate bool, + session entity.Session, +) (value *po.RepositoryNodeInstallSource, err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + if forUpdate { + tx = tx.Clauses(clause.Locking{Strength: "UPDATE"}) + } + value = new(po.RepositoryNodeInstallSource) + err = tx.Where("project_id = ? AND architecture = ?", projectID, architecture).Take(value).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrNodeInstallSourceNotFound + } + return value, err +} + +func (r *lifecycleRepository) ListInstallSources(projectID int64, session entity.Session) ( + values []po.RepositoryNodeInstallSource, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + err = tx.Where("project_id = ?", projectID).Order("architecture ASC, id ASC").Find(&values).Error + return values, err +} + +func (r *lifecycleRepository) CreateAllowlist(value *po.RepositoryNodeRegistrationAllowlist, + session entity.Session, +) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + return tx.Create(value).Error +} + +func (r *lifecycleRepository) UpdateAllowlist(value *po.RepositoryNodeRegistrationAllowlist, + session entity.Session, +) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + return tx.Save(value).Error +} + +func (r *lifecycleRepository) DeleteAllowlist(id string, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + result := tx.Where("id = ?", id).Delete(&po.RepositoryNodeRegistrationAllowlist{}) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return ErrNodeAllowlistNotFound + } + return nil +} + +func (r *lifecycleRepository) GetAllowlist(projectID int64, agentID uint64, forUpdate bool, + session entity.Session, +) (value *po.RepositoryNodeRegistrationAllowlist, err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + if forUpdate { + tx = tx.Clauses(clause.Locking{Strength: "UPDATE"}) + } + value = new(po.RepositoryNodeRegistrationAllowlist) + err = tx.Where("project_id = ? AND tms_agent_id = ?", projectID, agentID).Take(value).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrNodeAllowlistNotFound + } + return value, err +} + +func (r *lifecycleRepository) ListAllowlists(projectID int64, session entity.Session) ( + values []po.RepositoryNodeRegistrationAllowlist, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + err = tx.Where("project_id = ?", projectID).Order("tms_agent_id ASC, id ASC").Find(&values).Error + return values, err +} + +func (r *lifecycleRepository) CreateCertificate(value *po.RepositoryNodeCertificate, + session entity.Session, +) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + return tx.Create(value).Error +} + +func (r *lifecycleRepository) FindCertificateByAuthorizationID(authorizationID string, session entity.Session) ( + value *po.RepositoryNodeCertificate, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + value = new(po.RepositoryNodeCertificate) + err = tx.Where("authorization_id = ?", authorizationID).Take(value).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrNodeCertificateNotFound + } + return value, err +} + +func (r *lifecycleRepository) SupersedeActiveCertificates(nodeID string, graceUntil time.Time, + session entity.Session, +) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + return tx.Model(&po.RepositoryNodeCertificate{}). + Where("node_id = ? AND status = ?", nodeID, "ACTIVE"). + Updates(map[string]interface{}{"status": "SUPERSEDED", "grace_until": graceUntil}).Error +} + +func (r *lifecycleRepository) FindUsableCertificate(nodeID, fingerprint string, now time.Time, + session entity.Session, +) (value *po.RepositoryNodeCertificate, err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + value = new(po.RepositoryNodeCertificate) + err = tx.Where("node_id = ? AND fingerprint = ? AND not_before <= ? AND not_after > ?", nodeID, + fingerprint, now, now). + Where("status = ? OR (status = ? AND grace_until > ?)", "ACTIVE", "SUPERSEDED", now). + Take(value).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrNodeCertificateNotFound + } + return value, err +} + +func (r *lifecycleRepository) CountCertificates(nodeID string, session entity.Session) (count int64, err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + err = tx.Model(&po.RepositoryNodeCertificate{}).Where("node_id = ?", nodeID).Count(&count).Error + return count, err +} + +func (r *lifecycleRepository) RevokeCertificates(nodeID string, at time.Time, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + return tx.Model(&po.RepositoryNodeCertificate{}).Where("node_id = ? AND status IN ?", nodeID, + []string{"ACTIVE", "SUPERSEDED"}). + Updates(map[string]interface{}{"status": "REVOKED", "grace_until": at}).Error +} + +func (r *lifecycleRepository) CreateArtifact(value *po.RepositoryNodeArtifact, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + return tx.Create(value).Error +} + +func (r *lifecycleRepository) UpdateArtifact(value *po.RepositoryNodeArtifact, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + return tx.Save(value).Error +} + +func (r *lifecycleRepository) GetArtifactByID(id string, session entity.Session) ( + value *po.RepositoryNodeArtifact, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + value = new(po.RepositoryNodeArtifact) + err = tx.Where("id = ?", id).Take(value).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrNodeArtifactNotFound + } + return value, err +} + +func (r *lifecycleRepository) GetArtifact(version, release, architecture string, session entity.Session) ( + value *po.RepositoryNodeArtifact, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + value = new(po.RepositoryNodeArtifact) + err = tx.Where("version = ? AND `release` = ? AND architecture = ?", version, release, architecture). + Take(value).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrNodeArtifactNotFound + } + return value, err +} + +func (r *lifecycleRepository) ListArtifacts(status string, session entity.Session) ( + values []po.RepositoryNodeArtifact, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + if status != "" { + tx = tx.Where("status = ?", status) + } + err = tx.Order("created_at DESC, architecture ASC").Find(&values).Error + return values, err +} + +func (r *lifecycleRepository) ListArtifactCandidates(projectID int64, architecture, sourceKind, status string, + session entity.Session, +) (values []po.RepositoryNodeArtifact, err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + err = tx.Where("project_id = ? AND architecture = ? AND source_kind = ? AND status = ?", + projectID, architecture, sourceKind, status). + Order("name ASC, epoch ASC, version ASC, `release` ASC, created_at DESC, id ASC").Find(&values).Error + return values, err +} diff --git a/manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/lifecycle_mysql_integration_test.go b/manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/lifecycle_mysql_integration_test.go new file mode 100644 index 0000000000000000000000000000000000000000..5c2cb741f326cbc150b96e64a6cdeaf2681497f7 --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/lifecycle_mysql_integration_test.go @@ -0,0 +1,177 @@ +package repositorystoragenoderepo + +import ( + "context" + "database/sql" + "fmt" + "os" + "testing" + "time" + + _ "github.com/go-sql-driver/mysql" + "github.com/google/uuid" +) + +const bootstrapRaceLockHold = 300 * time.Millisecond + +func TestAllowlistDisableAndBootstrapAuthorizationSerialize(t *testing.T) { + dsn := os.Getenv("REPOSITORY_TEST_MYSQL_DSN") + if dsn == "" { + t.Skip("REPOSITORY_TEST_MYSQL_DSN is required for the MySQL row-lock integration test") + } + db, err := sql.Open("mysql", dsn) + if err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + if err = db.PingContext(ctx); err != nil { + t.Fatal(err) + } + + id := uuid.NewString() + agentID := uint64(time.Now().UnixNano()) + _, err = db.ExecContext(ctx, `INSERT INTO repository_node_registration_allowlists + (id, project_id, tms_agent_id, expected_architecture, status, created_by, updated_by) + VALUES (?, ?, ?, 'x86_64', 'ENABLED', 'p12-race-test', 'p12-race-test')`, id, 1, agentID) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if _, cleanupErr := db.Exec(`DELETE FROM repository_node_registration_allowlists WHERE id = ?`, id); cleanupErr != nil { + t.Errorf("delete temporary allowlist: %v", cleanupErr) + } + if closeErr := db.Close(); closeErr != nil { + t.Errorf("close integration database: %v", closeErr) + } + }) + + t.Run("authorization commits before disable", func(t *testing.T) { + first, err := db.BeginTx(ctx, nil) + if err != nil { + t.Fatal(err) + } + var status string + if err = first.QueryRowContext(ctx, + `SELECT status FROM repository_node_registration_allowlists WHERE id = ? FOR UPDATE`, id, + ).Scan(&status); err != nil { + _ = first.Rollback() + t.Fatal(err) + } + if status != "ENABLED" { + _ = first.Rollback() + t.Fatalf("authorization observed status %q", status) + } + + result := make(chan error, 1) + started := time.Now() + go func() { + result <- updateAllowlistStatusWithLock(ctx, db, id, "DISABLED") + }() + time.Sleep(bootstrapRaceLockHold) + if err = first.Commit(); err != nil { + t.Fatal(err) + } + if err = <-result; err != nil { + t.Fatal(err) + } + if waited := time.Since(started); waited < bootstrapRaceLockHold { + t.Fatalf("disable did not wait for authorization lock: %s", waited) + } + }) + + if _, err = db.ExecContext(ctx, + `UPDATE repository_node_registration_allowlists SET status = 'ENABLED' WHERE id = ?`, id, + ); err != nil { + t.Fatal(err) + } + + t.Run("disable commits before authorization", func(t *testing.T) { + first, err := db.BeginTx(ctx, nil) + if err != nil { + t.Fatal(err) + } + var current string + if err = first.QueryRowContext(ctx, + `SELECT status FROM repository_node_registration_allowlists WHERE id = ? FOR UPDATE`, id, + ).Scan(¤t); err != nil { + _ = first.Rollback() + t.Fatal(err) + } + if current != "ENABLED" { + _ = first.Rollback() + t.Fatalf("disable observed status %q", current) + } + if _, err = first.ExecContext(ctx, + `UPDATE repository_node_registration_allowlists SET status = 'DISABLED' WHERE id = ?`, id, + ); err != nil { + _ = first.Rollback() + t.Fatal(err) + } + + type authorizationResult struct { + status string + err error + } + result := make(chan authorizationResult, 1) + started := time.Now() + go func() { + tx, beginErr := db.BeginTx(ctx, nil) + if beginErr != nil { + result <- authorizationResult{err: beginErr} + return + } + var observed string + queryErr := tx.QueryRowContext(ctx, + `SELECT status FROM repository_node_registration_allowlists WHERE id = ? FOR UPDATE`, id, + ).Scan(&observed) + if queryErr == nil { + queryErr = tx.Commit() + } else { + _ = tx.Rollback() + } + result <- authorizationResult{status: observed, err: queryErr} + }() + time.Sleep(bootstrapRaceLockHold) + if err = first.Commit(); err != nil { + t.Fatal(err) + } + observed := <-result + if observed.err != nil { + t.Fatal(observed.err) + } + if waited := time.Since(started); waited < bootstrapRaceLockHold { + t.Fatalf("authorization did not wait for disable lock: %s", waited) + } + if observed.status != "DISABLED" { + t.Fatalf("authorization observed status %q after disable commit", observed.status) + } + }) +} + +func updateAllowlistStatusWithLock(ctx context.Context, db *sql.DB, id, status string) (err error) { + tx, err := db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer func() { + if err != nil { + _ = tx.Rollback() + } + }() + var current string + if err = tx.QueryRowContext(ctx, + `SELECT status FROM repository_node_registration_allowlists WHERE id = ? FOR UPDATE`, id, + ).Scan(¤t); err != nil { + return err + } + if current != "ENABLED" { + return fmt.Errorf("allowlist status before disable = %q", current) + } + if _, err = tx.ExecContext(ctx, + `UPDATE repository_node_registration_allowlists SET status = ? WHERE id = ?`, status, id, + ); err != nil { + return err + } + return tx.Commit() +} diff --git a/manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/lifecycle_test.go b/manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/lifecycle_test.go new file mode 100644 index 0000000000000000000000000000000000000000..4e240125a6f444d7125882e5b458fbc4c71ea4a6 --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/lifecycle_test.go @@ -0,0 +1,247 @@ +package repositorystoragenoderepo + +import ( + "database/sql" + "regexp" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/require" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/baserepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +type lifecycleTestSessionFactory struct{ db *gorm.DB } + +func (f lifecycleTestSessionFactory) NewSession() entity.Session { return f.db.Begin() } +func (f lifecycleTestSessionFactory) DB() entity.Session { return f.db } + +func newMockLifecycleRepository(t *testing.T) (*lifecycleRepository, sqlmock.Sqlmock, *gorm.DB, *sql.DB) { + t.Helper() + sqlDB, mock, err := sqlmock.New() + require.NoError(t, err) + db, err := gorm.Open(mysql.New(mysql.Config{ + Conn: sqlDB, SkipInitializeWithVersion: true, + }), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) + require.NoError(t, err) + return &lifecycleRepository{DBBaseRepository: baserepo.NewDBBaseRepository( + lifecycleTestSessionFactory{db: db}, nil, + )}, mock, db, sqlDB +} + +func TestListActiveInstallationsIncludesPendingEnrollmentWithoutTMSTask(t *testing.T) { + repository, mock, db, sqlDB := newMockLifecycleRepository(t) + + mock.ExpectQuery(regexp.QuoteMeta( + "SELECT * FROM `repository_node_enrollments` WHERE status IN (?,?,?,?,?,?) ORDER BY updated_at ASC LIMIT 100", + )).WithArgs("PENDING", "INSTALLING", "AUTHORIZED", "REGISTERING", "REGISTERED", "BOOTSTRAPPED"). + WillReturnRows(sqlmock.NewRows([]string{"id", "status", "tms_task_id"}). + AddRow("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", "PENDING", "")) + + values, err := repository.ListActiveInstallations(100, db) + require.NoError(t, err) + require.Len(t, values, 1) + require.Equal(t, "PENDING", values[0].Status) + require.Empty(t, values[0].TMSTaskID) + + mock.ExpectClose() + require.NoError(t, sqlDB.Close()) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestInstallSourceCRUDListAndLock(t *testing.T) { + repository, mock, db, sqlDB := newMockLifecycleRepository(t) + session := db.Session(&gorm.Session{SkipDefaultTransaction: true}) + value := &po.RepositoryNodeInstallSource{ + ID: "11111111-1111-4111-8111-111111111111", ProjectID: 42, + RepositoryID: "22222222-2222-4222-8222-222222222222", Architecture: "x86_64", + Status: "ENABLED", CreatedBy: "tester", UpdatedBy: "tester", + } + + mock.ExpectExec("INSERT INTO `repository_node_install_sources`"). + WillReturnResult(sqlmock.NewResult(1, 1)) + require.NoError(t, repository.CreateInstallSource(value, session)) + + value.Status = "DISABLED" + mock.ExpectExec("UPDATE `repository_node_install_sources` SET"). + WillReturnResult(sqlmock.NewResult(0, 1)) + require.NoError(t, repository.UpdateInstallSource(value, session)) + + mock.ExpectQuery(regexp.QuoteMeta( + "SELECT * FROM `repository_node_install_sources` WHERE project_id = ? AND architecture = ? LIMIT 1 FOR UPDATE", + )).WithArgs(int64(42), "x86_64"). + WillReturnRows(sqlmock.NewRows([]string{"id", "project_id", "repository_id", "architecture", "status"}). + AddRow(value.ID, value.ProjectID, value.RepositoryID, value.Architecture, value.Status)) + + locked, err := repository.GetInstallSource(42, "x86_64", true, session) + require.NoError(t, err) + require.Equal(t, value.ID, locked.ID) + require.Equal(t, "DISABLED", locked.Status) + + mock.ExpectQuery(regexp.QuoteMeta( + "SELECT * FROM `repository_node_install_sources` WHERE project_id = ? ORDER BY architecture ASC, id ASC", + )).WithArgs(int64(42)). + WillReturnRows(sqlmock.NewRows([]string{"id", "project_id", "architecture"}). + AddRow(value.ID, value.ProjectID, value.Architecture)) + + values, err := repository.ListInstallSources(42, session) + require.NoError(t, err) + require.Len(t, values, 1) + + mock.ExpectExec(regexp.QuoteMeta( + "DELETE FROM `repository_node_install_sources` WHERE id = ?", + )).WithArgs(value.ID).WillReturnResult(sqlmock.NewResult(0, 1)) + require.NoError(t, repository.DeleteInstallSource(value.ID, session)) + + mock.ExpectClose() + require.NoError(t, sqlDB.Close()) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestInstallSourceNotFoundIsExplicit(t *testing.T) { + repository, mock, db, sqlDB := newMockLifecycleRepository(t) + session := db.Session(&gorm.Session{SkipDefaultTransaction: true}) + + mock.ExpectQuery(regexp.QuoteMeta( + "SELECT * FROM `repository_node_install_sources` WHERE project_id = ? AND architecture = ? LIMIT 1", + )).WithArgs(int64(42), "aarch64").WillReturnRows(sqlmock.NewRows([]string{"id"})) + + value, err := repository.GetInstallSource(42, "aarch64", false, session) + require.Nil(t, value) + require.ErrorIs(t, err, ErrNodeInstallSourceNotFound) + + mock.ExpectExec(regexp.QuoteMeta( + "DELETE FROM `repository_node_install_sources` WHERE id = ?", + )).WithArgs("missing").WillReturnResult(sqlmock.NewResult(0, 0)) + require.ErrorIs(t, repository.DeleteInstallSource("missing", session), ErrNodeInstallSourceNotFound) + + mock.ExpectClose() + require.NoError(t, sqlDB.Close()) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestAllowlistCRUDListAndLock(t *testing.T) { + repository, mock, db, sqlDB := newMockLifecycleRepository(t) + session := db.Session(&gorm.Session{SkipDefaultTransaction: true}) + value := &po.RepositoryNodeRegistrationAllowlist{ + ID: "33333333-3333-4333-8333-333333333333", ProjectID: 42, TMSAgentID: 99, + ExpectedArchitecture: "aarch64", Status: "ENABLED", CreatedBy: "tester", UpdatedBy: "tester", + } + + mock.ExpectExec("INSERT INTO `repository_node_registration_allowlists`"). + WillReturnResult(sqlmock.NewResult(1, 1)) + require.NoError(t, repository.CreateAllowlist(value, session)) + + value.Status = "DISABLED" + mock.ExpectExec("UPDATE `repository_node_registration_allowlists` SET"). + WillReturnResult(sqlmock.NewResult(0, 1)) + require.NoError(t, repository.UpdateAllowlist(value, session)) + + mock.ExpectQuery(regexp.QuoteMeta( + "SELECT * FROM `repository_node_registration_allowlists` WHERE project_id = ? AND tms_agent_id = ? LIMIT 1 FOR UPDATE", + )).WithArgs(int64(42), uint64(99)). + WillReturnRows(sqlmock.NewRows([]string{"id", "project_id", "tms_agent_id", "status"}). + AddRow(value.ID, value.ProjectID, value.TMSAgentID, value.Status)) + + locked, err := repository.GetAllowlist(42, 99, true, session) + require.NoError(t, err) + require.Equal(t, value.ID, locked.ID) + require.Equal(t, "DISABLED", locked.Status) + + mock.ExpectQuery(regexp.QuoteMeta( + "SELECT * FROM `repository_node_registration_allowlists` WHERE project_id = ? ORDER BY tms_agent_id ASC, id ASC", + )).WithArgs(int64(42)). + WillReturnRows(sqlmock.NewRows([]string{"id", "project_id", "tms_agent_id"}). + AddRow(value.ID, value.ProjectID, value.TMSAgentID)) + + values, err := repository.ListAllowlists(42, session) + require.NoError(t, err) + require.Len(t, values, 1) + + mock.ExpectExec(regexp.QuoteMeta( + "DELETE FROM `repository_node_registration_allowlists` WHERE id = ?", + )).WithArgs(value.ID).WillReturnResult(sqlmock.NewResult(0, 1)) + require.NoError(t, repository.DeleteAllowlist(value.ID, session)) + + mock.ExpectClose() + require.NoError(t, sqlDB.Close()) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestAllowlistNotFoundIsExplicit(t *testing.T) { + repository, mock, db, sqlDB := newMockLifecycleRepository(t) + session := db.Session(&gorm.Session{SkipDefaultTransaction: true}) + + mock.ExpectQuery(regexp.QuoteMeta( + "SELECT * FROM `repository_node_registration_allowlists` WHERE project_id = ? AND tms_agent_id = ? LIMIT 1", + )).WithArgs(int64(42), uint64(100)).WillReturnRows(sqlmock.NewRows([]string{"id"})) + + value, err := repository.GetAllowlist(42, 100, false, session) + require.Nil(t, value) + require.ErrorIs(t, err, ErrNodeAllowlistNotFound) + + mock.ExpectExec(regexp.QuoteMeta( + "DELETE FROM `repository_node_registration_allowlists` WHERE id = ?", + )).WithArgs("missing").WillReturnResult(sqlmock.NewResult(0, 0)) + require.ErrorIs(t, repository.DeleteAllowlist("missing", session), ErrNodeAllowlistNotFound) + + mock.ExpectClose() + require.NoError(t, sqlDB.Close()) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestFindCertificateByAuthorizationID(t *testing.T) { + repository, mock, db, sqlDB := newMockLifecycleRepository(t) + authorizationID := "44444444-4444-4444-8444-444444444444" + + mock.ExpectQuery(regexp.QuoteMeta( + "SELECT * FROM `repository_node_certificates` WHERE authorization_id = ? LIMIT 1", + )).WithArgs(authorizationID). + WillReturnRows(sqlmock.NewRows([]string{"id", "authorization_id", "status"}). + AddRow("55555555-5555-4555-8555-555555555555", authorizationID, "ACTIVE")) + + value, err := repository.FindCertificateByAuthorizationID(authorizationID, db) + require.NoError(t, err) + require.Equal(t, authorizationID, value.AuthorizationID) + + mock.ExpectQuery(regexp.QuoteMeta( + "SELECT * FROM `repository_node_certificates` WHERE authorization_id = ? LIMIT 1", + )).WithArgs("missing").WillReturnRows(sqlmock.NewRows([]string{"id"})) + + value, err = repository.FindCertificateByAuthorizationID("missing", db) + require.Nil(t, value) + require.ErrorIs(t, err, ErrNodeCertificateNotFound) + + mock.ExpectClose() + require.NoError(t, sqlDB.Close()) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestListArtifactCandidatesUsesProjectCanonicalArchitectureSourceAndStatus(t *testing.T) { + repository, mock, db, sqlDB := newMockLifecycleRepository(t) + createdAt := time.Date(2026, time.October, 1, 0, 0, 0, 0, time.UTC) + + mock.ExpectQuery(regexp.QuoteMeta( + "SELECT * FROM `repository_node_artifacts` WHERE project_id = ? AND architecture = ? AND source_kind = ? AND status = ? ORDER BY name ASC, epoch ASC, version ASC, `release` ASC, created_at DESC, id ASC", + )).WithArgs(int64(42), "aarch64", "REPOSITORY_REVISION", "AVAILABLE"). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "project_id", "architecture", "source_kind", "status", "name", "epoch", "version", "release", "created_at", + }).AddRow("66666666-6666-4666-8666-666666666666", 42, "aarch64", "REPOSITORY_REVISION", + "AVAILABLE", "ocmanager-repository-node", 0, "1.0.0", "1", createdAt)) + + values, err := repository.ListArtifactCandidates(42, "aarch64", "REPOSITORY_REVISION", "AVAILABLE", db) + require.NoError(t, err) + require.Len(t, values, 1) + require.Equal(t, int64(42), values[0].ProjectID) + require.Equal(t, "aarch64", values[0].Architecture) + + mock.ExpectClose() + require.NoError(t, sqlDB.Close()) + require.NoError(t, mock.ExpectationsWereMet()) +} diff --git a/manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/repository.go b/manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/repository.go new file mode 100644 index 0000000000000000000000000000000000000000..fc6f6dda0424dbb097c6e301d42cc82a44295b1b --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/repository.go @@ -0,0 +1,395 @@ +package repositorystoragenoderepo + +import ( + "errors" + "fmt" + "time" + + "github.com/go-sql-driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/clause" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/baserepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +var ( + ErrStorageNodeNotFound = errors.New("repository storage node not found") + ErrStorageNodeDuplicate = errors.New("repository storage node already exists in this project") + ErrPreflightNotFound = errors.New("repository preflight record not found") + ErrTaskLogNotFound = errors.New("repository task log index not found") +) + +type Repository interface { + SyncTables() error + CreateNode(*po.RepositoryStorageNode, entity.Session) error + UpdateNode(*po.RepositoryStorageNode, entity.Session) error + GetNodeByID(string, entity.Session) (*po.RepositoryStorageNode, error) + ListNodes(request.ListRepositoryStorageNodesRequest, entity.Session) ([]po.RepositoryStorageNode, request.PageResponse, error) + DeleteNode(string, entity.Session) error + CountAssignedRepositories(string, entity.Session) (int64, error) + MarkOfflineBefore(time.Time, entity.Session) (int64, error) + CreatePreflight(*po.RepositoryPreflightRecord, entity.Session) error + GetPreflightByID(string, entity.Session) (*po.RepositoryPreflightRecord, error) + ListPreflights(request.ListRepositoryPreflightRecordsRequest, entity.Session) ([]po.RepositoryPreflightRecord, request.PageResponse, error) + UpsertTaskLogIndex(*po.RepositoryTaskLogIndex, entity.Session) error + GetTaskLogIndex(string, entity.Session) (*po.RepositoryTaskLogIndex, error) +} + +// StatisticsRepository is the production read model for the node overview. +// Keeping it separate preserves the small Repository contract used by focused +// domain test doubles. +type StatisticsRepository interface { + Statistics(request.ListRepositoryStorageNodesRequest, entity.Session) (request.RepositoryStorageNodeStatistics, error) +} + +func (r *repository) UpsertTaskLogIndex(value *po.RepositoryTaskLogIndex, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + return tx.Save(value).Error +} + +func (r *repository) GetTaskLogIndex(taskID string, session entity.Session) ( + value *po.RepositoryTaskLogIndex, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + value = new(po.RepositoryTaskLogIndex) + if err = tx.Where("task_id = ?", taskID).Take(value).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrTaskLogNotFound + } + return nil, err + } + return value, nil +} + +type repository struct{ baserepo.DBBaseRepository } + +func New(client *database.DBClient) Repository { + return &repository{DBBaseRepository: baserepo.NewDBBaseRepository(client, []interface{}{ + new(po.RepositoryStorageNode), new(po.RepositoryPreflightRecord), + new(po.RepositoryTaskLogIndex), new(po.RepositoryAIAnalysis), new(po.RepositoryNodeRepositoryUsage), + })} +} + +func (r *repository) CreateNode(value *po.RepositoryStorageNode, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + if err = tx.Create(value).Error; err != nil { + return normalizeNodeWriteError(err) + } + return nil +} + +func (r *repository) UpdateNode(value *po.RepositoryStorageNode, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + result := tx.Model(&po.RepositoryStorageNode{}).Where("id = ?", value.ID).Updates(map[string]interface{}{ + "name": value.Name, "description": value.Description, "control_base_url": value.ControlBaseURL, + "publish_base_url": value.PublishBaseURL, "tls_server_name": value.TLSServerName, + "certificate_fingerprint": value.CertificateFingerprint, "status": value.Status, "enabled": value.Enabled, + "filesystem_type": value.FilesystemType, "capacity_bytes": value.CapacityBytes, + "free_bytes": value.FreeBytes, "free_inodes": value.FreeInodes, "tool_versions": value.ToolVersionsJSON, + "labels": value.LabelsJSON, "last_heartbeat_at": value.LastHeartbeatAt, "last_probe_at": value.LastProbeAt, + "last_error_code": value.LastErrorCode, "last_error_message": value.LastErrorMessage, + "hostname": value.Hostname, "ip_address": value.IPAddress, "architecture": value.Architecture, + "os_name": value.OSName, "os_version": value.OSVersion, "agent_version": value.AgentVersion, + }) + if result.Error != nil { + return normalizeNodeWriteError(result.Error) + } + if result.RowsAffected == 0 { + return ErrStorageNodeNotFound + } + return nil +} + +func (r *repository) GetNodeByID(id string, session entity.Session) (_ *po.RepositoryStorageNode, err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + value := new(po.RepositoryStorageNode) + if err = tx.Where("id = ?", id).Take(value).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrStorageNodeNotFound + } + return nil, err + } + return value, nil +} + +func (r *repository) ListNodes(filter request.ListRepositoryStorageNodesRequest, session entity.Session) ( + values []po.RepositoryStorageNode, page request.PageResponse, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + tx = applyStorageNodeListFilters(tx.Model(&po.RepositoryStorageNode{}), filter) + var count int64 + if err = tx.Count(&count).Error; err != nil { + return nil, page, err + } + limit, offset := filter.PageRequest.Limit() + err = tx.Order("name ASC, id ASC").Offset(offset).Limit(limit).Find(&values).Error + if err == nil { + err = enrichNodeMetrics(tx, values) + } + return values, filter.PageRequest.Response(count), err +} + +func (r *repository) Statistics(filter request.ListRepositoryStorageNodesRequest, session entity.Session) ( + statistics request.RepositoryStorageNodeStatistics, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + type nodeStatistics struct { + TotalNodes int64 `gorm:"column:total_nodes"` + OnlineNodes int64 `gorm:"column:online_nodes"` + TotalCapacityBytes int64 `gorm:"column:total_capacity_bytes"` + UsedCapacityBytes int64 `gorm:"column:used_capacity_bytes"` + CriticalNodes int64 `gorm:"column:critical_nodes"` + } + var result nodeStatistics + err = applyStorageNodeListFilters(tx.Model(&po.RepositoryStorageNode{}), filter). + Select(` + COUNT(*) AS total_nodes, + COALESCE(SUM(CASE WHEN status = 'ONLINE' THEN 1 ELSE 0 END), 0) AS online_nodes, + COALESCE(SUM(capacity_bytes), 0) AS total_capacity_bytes, + COALESCE(SUM(CASE WHEN capacity_bytes > free_bytes THEN capacity_bytes - free_bytes ELSE 0 END), 0) AS used_capacity_bytes, + COALESCE(SUM(CASE WHEN capacity_bytes > 0 AND (capacity_bytes - free_bytes) * 100 >= capacity_bytes * 90 THEN 1 ELSE 0 END), 0) AS critical_nodes`). + Scan(&result).Error + if err != nil { + return statistics, err + } + return request.RepositoryStorageNodeStatistics{ + TotalNodes: result.TotalNodes, OnlineNodes: result.OnlineNodes, + TotalCapacityBytes: result.TotalCapacityBytes, UsedCapacityBytes: result.UsedCapacityBytes, + CriticalNodes: result.CriticalNodes, + }, nil +} + +func applyStorageNodeListFilters(tx *gorm.DB, filter request.ListRepositoryStorageNodesRequest) *gorm.DB { + tx = tx.Where("project_id = ?", filter.ProjectID) + if filter.Status != "" { + tx = tx.Where("status = ?", filter.Status) + } + if filter.Keyword != "" { + keyword := "%" + filter.Keyword + "%" + tx = tx.Where("name LIKE ? OR description LIKE ?", keyword, keyword) + } + return tx +} + +func enrichNodeMetrics(tx *gorm.DB, values []po.RepositoryStorageNode) error { + if len(values) == 0 { + return nil + } + ids := make([]string, 0, len(values)) + positions := make(map[string]int, len(values)) + lookup := tx.Session(&gorm.Session{NewDB: true}) + for index := range values { + ids = append(ids, values[index].ID) + positions[values[index].ID] = index + } + type nodeCount struct { + StorageNodeID string `gorm:"column:storage_node_id"` + Count int64 `gorm:"column:repository_count"` + } + var counts []nodeCount + if err := lookup.Model(&po.SoftwareRepository{}). + Select("primary_storage_node_id AS storage_node_id, COUNT(*) AS repository_count"). + Where("primary_storage_node_id IN ?", ids).Group("primary_storage_node_id").Scan(&counts).Error; err != nil { + return err + } + for _, item := range counts { + if index, ok := positions[item.StorageNodeID]; ok { + values[index].RepositoryCount = item.Count + } + } + type nodeUsage struct { + StorageNodeID string `gorm:"column:storage_node_id"` + PhysicalBytes int64 `gorm:"column:physical_bytes"` + } + var usages []nodeUsage + if err := lookup.Table("repository_node_repository_usages AS node_usage"). + Select("node_usage.storage_node_id, COALESCE(SUM(node_usage.physical_bytes), 0) AS physical_bytes"). + Joins("JOIN software_repositories AS repository ON repository.id = node_usage.repository_id AND repository.primary_storage_node_id = node_usage.storage_node_id"). + Where("node_usage.storage_node_id IN ?", ids).Group("node_usage.storage_node_id").Scan(&usages).Error; err != nil { + return err + } + for _, item := range usages { + if index, ok := positions[item.StorageNodeID]; ok { + values[index].RepositoryPhysicalBytes = item.PhysicalBytes + } + } + return nil +} + +func (r *repository) ReplaceRepositoryUsages(storageNodeID string, values []po.RepositoryNodeRepositoryUsage, + session entity.Session, +) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + ids := make([]string, 0, len(values)) + for _, value := range values { + ids = append(ids, value.RepositoryID) + } + remove := tx.Where("storage_node_id = ?", storageNodeID) + if len(ids) > 0 { + remove = remove.Where("repository_id NOT IN ?", ids) + } + if err = remove.Delete(&po.RepositoryNodeRepositoryUsage{}).Error; err != nil || len(values) == 0 { + return err + } + return tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "storage_node_id"}, {Name: "repository_id"}}, + DoUpdates: clause.AssignmentColumns([]string{"logical_bytes", "physical_bytes", "publish_status", "observed_at"}), + }).Create(&values).Error +} + +func (r *repository) ListNodeRepositorySummaries(storageNodeID string, + session entity.Session, +) (values []po.RepositoryNodeRepositorySummary, err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + lookup := tx.Session(&gorm.Session{NewDB: true}) + err = lookup.Table("software_repositories AS r").Select(` + r.id AS repository_id, r.name, r.system_product, r.product_version, + r.repository_type, r.architecture, r.upstream_url, + COALESCE(rev.package_count, 0) AS package_count, + COALESCE(rev.total_bytes, 0) AS logical_bytes, + COALESCE(node_usage.physical_bytes, 0) AS physical_bytes, + r.active_revision_id AS current_revision, r.status AS publish_status`). + Joins("LEFT JOIN repository_revisions AS rev ON rev.id = r.active_revision_id"). + Joins("LEFT JOIN repository_node_repository_usages AS node_usage ON node_usage.storage_node_id = ? AND node_usage.repository_id = r.id", storageNodeID). + Where("r.primary_storage_node_id = ?", storageNodeID). + Order("r.system_product ASC, r.product_version ASC, r.name ASC, r.architecture ASC").Scan(&values).Error + if err != nil || len(values) == 0 { + return values, err + } + repositoryIDs := make([]string, 0, len(values)) + positions := make(map[string]int, len(values)) + for index := range values { + repositoryIDs = append(repositoryIDs, values[index].RepositoryID) + positions[values[index].RepositoryID] = index + } + var tasks []po.RepositoryTask + if err = lookup.Where("repository_id IN ? AND task_type = ?", repositoryIDs, request.RepositoryTaskTypeSync). + Order("repository_id ASC, created_at DESC, id DESC").Find(&tasks).Error; err != nil { + return nil, err + } + lastSeen := make(map[string]struct{}, len(values)) + for index := range tasks { + task := &tasks[index] + position, ok := positions[task.RepositoryID] + if !ok { + continue + } + if task.Status == request.RepositoryTaskStatusPending || task.Status == request.RepositoryTaskStatusRunning || + task.Status == request.RepositoryTaskStatusCancelRequested { + if values[position].RunningTaskID == "" { + values[position].RunningTaskID = task.ID + } + continue + } + if _, ok = lastSeen[task.RepositoryID]; ok { + continue + } + lastSeen[task.RepositoryID] = struct{}{} + values[position].LastSyncAt = task.FinishedAt + values[position].LastSyncStatus = task.Status + if task.Status == request.RepositoryTaskStatusFailed { + values[position].LastSyncErrorCode = task.ErrorCode + values[position].LastSyncErrorMessage = task.ErrorMessage + } + } + var schedules []po.RepositorySyncSchedule + if err = lookup.Where("repository_id IN ? AND status = ?", repositoryIDs, + request.RepositoryScheduleStatusEnabled).Find(&schedules).Error; err != nil { + return nil, err + } + for index := range schedules { + if position, ok := positions[schedules[index].RepositoryID]; ok { + next := schedules[index].NextRunAt + values[position].NextSyncAt = &next + } + } + return values, nil +} + +func (r *repository) DeleteNode(id string, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + result := tx.Where("id = ?", id).Delete(&po.RepositoryStorageNode{}) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return ErrStorageNodeNotFound + } + return nil +} + +func (r *repository) CountAssignedRepositories(id string, session entity.Session) (count int64, err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + err = tx.Model(&po.SoftwareRepository{}).Where("primary_storage_node_id = ?", id).Count(&count).Error + return count, err +} + +func (r *repository) MarkOfflineBefore(cutoff time.Time, session entity.Session) (count int64, err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + result := tx.Model(&po.RepositoryStorageNode{}). + Where("enabled = ? AND status = ? AND (last_heartbeat_at IS NULL OR last_heartbeat_at < ?)", + true, request.RepositoryStorageNodeStatusOnline, cutoff). + Update("status", request.RepositoryStorageNodeStatusOffline) + return result.RowsAffected, result.Error +} + +func (r *repository) CreatePreflight(value *po.RepositoryPreflightRecord, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + return tx.Create(value).Error +} + +func (r *repository) GetPreflightByID(id string, session entity.Session) (_ *po.RepositoryPreflightRecord, err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + value := new(po.RepositoryPreflightRecord) + if err = tx.Where("id = ?", id).Take(value).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrPreflightNotFound + } + return nil, err + } + return value, nil +} + +func (r *repository) ListPreflights(filter request.ListRepositoryPreflightRecordsRequest, session entity.Session) ( + values []po.RepositoryPreflightRecord, page request.PageResponse, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + tx = tx.Model(&po.RepositoryPreflightRecord{}).Where("storage_node_id = ?", filter.StorageNodeID) + if filter.RepositoryID != "" { + tx = tx.Where("repository_id = ?", filter.RepositoryID) + } + var count int64 + if err = tx.Count(&count).Error; err != nil { + return nil, page, err + } + limit, offset := filter.PageRequest.Limit() + err = tx.Order("created_at DESC, id DESC").Offset(offset).Limit(limit).Find(&values).Error + return values, filter.PageRequest.Response(count), err +} + +func normalizeNodeWriteError(err error) error { + var mysqlErr *mysql.MySQLError + if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 { + return ErrStorageNodeDuplicate + } + return fmt.Errorf("write repository storage node: %w", err) +} diff --git a/manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/statistics_test.go b/manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/statistics_test.go new file mode 100644 index 0000000000000000000000000000000000000000..7b8f4b34ad494d42bcbc794eb668d3148e963375 --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/repositorystoragenoderepo/statistics_test.go @@ -0,0 +1,57 @@ +package repositorystoragenoderepo + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/require" + "gorm.io/gorm" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/baserepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +func TestStorageNodeStatisticsUseFullFilteredSet(t *testing.T) { + _, mock, db, sqlDB := newMockLifecycleRepository(t) + repository := &repository{DBBaseRepository: baserepo.NewDBBaseRepository( + lifecycleTestSessionFactory{db: db}, nil, + )} + filter := request.ListRepositoryStorageNodesRequest{ + ProjectID: 42, + Status: request.RepositoryStorageNodeStatusOnline, + Keyword: "edge", + PageRequest: request.PageRequest{ + PageNumber: 9, + PageSize: 1, + }, + } + + // No LIMIT/OFFSET is part of this aggregate: it describes the complete + // filtered set, independently of the list page requested by the caller. + mock.ExpectQuery(`(?s)SELECT .*total_nodes.*online_nodes.*total_capacity_bytes.*used_capacity_bytes.*critical_nodes.*FROM .*repository_storage_nodes.*WHERE project_id = .*status = .*\(name LIKE .* OR description LIKE .*\)`). + WithArgs(int64(42), request.RepositoryStorageNodeStatusOnline, "%edge%", "%edge%"). + WillReturnRows(sqlmock.NewRows([]string{ + "total_nodes", "online_nodes", "total_capacity_bytes", "used_capacity_bytes", "critical_nodes", + }).AddRow(3, 3, 1000, 820, 1)) + + statistics, err := repository.Statistics(filter, db) + require.NoError(t, err) + require.Equal(t, request.RepositoryStorageNodeStatistics{ + TotalNodes: 3, OnlineNodes: 3, TotalCapacityBytes: 1000, + UsedCapacityBytes: 820, CriticalNodes: 1, + }, statistics) + require.NoError(t, mock.ExpectationsWereMet()) + mock.ExpectClose() + require.NoError(t, sqlDB.Close()) +} + +func TestStorageNodeStatisticsFilterDoesNotApplyPagination(t *testing.T) { + _, _, db, sqlDB := newMockLifecycleRepository(t) + defer sqlDB.Close() + statement := applyStorageNodeListFilters( + db.Session(&gorm.Session{DryRun: true}).Model(&po.RepositoryStorageNode{}), + request.ListRepositoryStorageNodesRequest{ProjectID: 1}, + ).Find(&[]po.RepositoryStorageNode{}).Statement + require.Equal(t, []interface{}{int64(1)}, statement.Vars) +} diff --git a/manager/backend/services/footstone/domain/repository/repositorytaskrepo/repository.go b/manager/backend/services/footstone/domain/repository/repositorytaskrepo/repository.go new file mode 100644 index 0000000000000000000000000000000000000000..7b590eba7c90a3b12f5855e45b12d966194e5ed1 --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/repositorytaskrepo/repository.go @@ -0,0 +1,362 @@ +package repositorytaskrepo + +import ( + "errors" + "fmt" + "time" + + "github.com/go-sql-driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/clause" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/baserepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +var ( + ErrTaskNotFound = errors.New("repository task not found") + ErrTaskDuplicate = errors.New("repository task already exists") +) + +type Repository interface { + SyncTables() error + Create(*po.RepositoryTask, entity.Session) error + Update(*po.RepositoryTask, entity.Session) error + GetByID(string, entity.Session) (*po.RepositoryTask, error) + GetByIDForUpdate(string, entity.Session) (*po.RepositoryTask, error) + GetByIdempotencyKey(string, entity.Session) (*po.RepositoryTask, error) + List(request.ListRepositoryTasksRequest, entity.Session) ([]po.RepositoryTask, request.PageResponse, error) + HasUnfinishedMutation(string, entity.Session) (bool, error) + HasUnfinishedType(string, string, entity.Session) (bool, error) + FindClaimCandidate(time.Time, string, []string, entity.Session) (*po.RepositoryTask, error) + FindExpired(time.Time, int, entity.Session) ([]po.RepositoryTask, error) +} + +// StatisticsRepository is implemented by the production task repository. It +// is kept separate from Repository so focused test doubles do not need to +// implement read-only product metrics they do not exercise. +type StatisticsRepository interface { + Statistics(request.ListRepositoryTasksRequest, entity.Session) (request.RepositoryTaskStatistics, error) +} + +// RetryRepository reads the current retry while the caller holds the original +// task row lock, so concurrent requests cannot enqueue duplicate active retries. +type RetryRepository interface { + FindUnfinishedRetry(*po.RepositoryTask, entity.Session) (*po.RepositoryTask, error) +} + +type TargetRepository interface { + SyncTables() error + CreateTarget(*po.RepositoryTaskTarget, entity.Session) error + UpdateTarget(*po.RepositoryTaskTarget, entity.Session) error + ListTargets(string, entity.Session) ([]po.RepositoryTaskTarget, error) +} + +func (r *repository) HasUnfinishedType(repositoryID, taskType string, session entity.Session) ( + exists bool, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + var count int64 + err = tx.Model(&po.RepositoryTask{}). + Where("repository_id = ? AND task_type = ? AND status IN ?", repositoryID, taskType, []string{ + request.RepositoryTaskStatusPending, + request.RepositoryTaskStatusRunning, + request.RepositoryTaskStatusCancelRequested, + }).Count(&count).Error + return count > 0, err +} + +func (r *repository) HasUnfinishedMutation(repositoryID string, session entity.Session) ( + exists bool, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + var count int64 + err = tx.Model(&po.RepositoryTask{}). + Where("repository_id = ? AND requires_mutation_lock = ? AND status IN ?", repositoryID, true, []string{ + request.RepositoryTaskStatusPending, + request.RepositoryTaskStatusRunning, + request.RepositoryTaskStatusCancelRequested, + }).Count(&count).Error + return count > 0, err +} + +type repository struct { + baserepo.DBBaseRepository +} + +func New(client *database.DBClient) Repository { + return &repository{DBBaseRepository: baserepo.NewDBBaseRepository(client, []interface{}{new(po.RepositoryTask)})} +} + +func NewTargetRepository(client *database.DBClient) TargetRepository { + return &repository{DBBaseRepository: baserepo.NewDBBaseRepository(client, []interface{}{new(po.RepositoryTaskTarget)})} +} + +func (r *repository) CreateTarget(value *po.RepositoryTaskTarget, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + return tx.Create(value).Error +} + +func (r *repository) UpdateTarget(value *po.RepositoryTaskTarget, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + return tx.Save(value).Error +} + +func (r *repository) ListTargets(taskID string, session entity.Session) ( + values []po.RepositoryTaskTarget, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + err = tx.Where("task_id = ?", taskID). + Order("CASE WHEN status IN ('FAILED', 'CANCELED') THEN 0 WHEN status = 'RUNNING' THEN 1 ELSE 2 END, id ASC"). + Find(&values).Error + return values, err +} + +func (r *repository) Create(value *po.RepositoryTask, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + if err = tx.Create(value).Error; err != nil { + var mysqlErr *mysql.MySQLError + if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 { + return ErrTaskDuplicate + } + return fmt.Errorf("create repository task: %w", err) + } + return nil +} + +func (r *repository) Update(value *po.RepositoryTask, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + if err = tx.Save(value).Error; err != nil { + return fmt.Errorf("update repository task: %w", err) + } + return nil +} + +func (r *repository) GetByID(id string, session entity.Session) (*po.RepositoryTask, error) { + return r.getByID(id, false, session) +} + +func (r *repository) GetByIDForUpdate(id string, session entity.Session) (*po.RepositoryTask, error) { + return r.getByID(id, true, session) +} + +func (r *repository) GetByIdempotencyKey(key string, session entity.Session) ( + value *po.RepositoryTask, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + value = new(po.RepositoryTask) + if err = tx.Where("idempotency_key = ?", key).Take(value).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrTaskNotFound + } + return nil, err + } + return value, nil +} + +func (r *repository) FindUnfinishedRetry(original *po.RepositoryTask, session entity.Session) ( + value *po.RepositoryTask, err error, +) { + if original == nil { + return nil, errors.New("original repository task is required") + } + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + value = new(po.RepositoryTask) + err = tx.Clauses(clause.Locking{Strength: "UPDATE"}). + Where("repository_id = ? AND project_id = ? AND retry_of_task_id = ? AND task_type = ? AND status IN ?", + original.RepositoryID, original.ProjectID, original.ID, original.Type, []string{ + request.RepositoryTaskStatusPending, + request.RepositoryTaskStatusRunning, + request.RepositoryTaskStatusCancelRequested, + }).Order("created_at ASC, id ASC").Take(value).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrTaskNotFound + } + if err != nil { + return nil, err + } + return value, nil +} + +func (r *repository) getByID(id string, forUpdate bool, session entity.Session) ( + value *po.RepositoryTask, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + if forUpdate { + tx = tx.Clauses(clause.Locking{Strength: "UPDATE"}) + } + value = new(po.RepositoryTask) + if err = tx.Where("id = ?", id).Take(value).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrTaskNotFound + } + return nil, err + } + return value, nil +} + +func (r *repository) List(filter request.ListRepositoryTasksRequest, session entity.Session) ( + values []po.RepositoryTask, page request.PageResponse, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + tx = applyTaskListFilters(tx.Model(&po.RepositoryTask{}), filter, true) + var count int64 + if err = tx.Count(&count).Error; err != nil { + return nil, page, err + } + limit, offset := filter.PageRequest.Limit() + if err = tx.Order("created_at DESC, id DESC").Offset(offset).Limit(limit).Find(&values).Error; err != nil { + return nil, page, err + } + if err = enrichTaskNames(tx, values); err != nil { + return nil, page, err + } + return values, filter.PageRequest.Response(count), nil +} + +func (r *repository) Statistics(filter request.ListRepositoryTasksRequest, session entity.Session) ( + statistics request.RepositoryTaskStatistics, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + type statusCounts struct { + PendingCount int64 `gorm:"column:pending_count"` + RunningCount int64 `gorm:"column:running_count"` + FailedCount int64 `gorm:"column:failed_count"` + SuccessCount int64 `gorm:"column:success_count"` + } + var counts statusCounts + err = applyTaskListFilters(tx.Model(&po.RepositoryTask{}), filter, false). + Select(` + COALESCE(SUM(CASE WHEN status = 'PENDING' THEN 1 ELSE 0 END), 0) AS pending_count, + COALESCE(SUM(CASE WHEN status IN ('RUNNING', 'CANCEL_REQUESTED') THEN 1 ELSE 0 END), 0) AS running_count, + COALESCE(SUM(CASE WHEN status IN ('FAILED', 'CANCELED') THEN 1 ELSE 0 END), 0) AS failed_count, + COALESCE(SUM(CASE WHEN status IN ('SUCCESS', 'PARTIAL_SUCCESS') THEN 1 ELSE 0 END), 0) AS success_count`). + Scan(&counts).Error + if err != nil { + return statistics, err + } + return request.RepositoryTaskStatistics{ + PendingCount: counts.PendingCount, + RunningCount: counts.RunningCount, + FailedCount: counts.FailedCount, + SuccessCount: counts.SuccessCount, + }, nil +} + +func applyTaskListFilters(tx *gorm.DB, filter request.ListRepositoryTasksRequest, includeStatus bool) *gorm.DB { + tx = tx.Where("project_id = ?", filter.ProjectID) + if filter.RepositoryID != "" { + tx = tx.Where("repository_id = ?", filter.RepositoryID) + } + if filter.StorageNodeID != "" { + tx = tx.Where("storage_node_id = ?", filter.StorageNodeID) + } + if filter.Type != "" { + tx = tx.Where("task_type = ?", filter.Type) + } + if filter.Trigger != "" { + tx = tx.Where("trigger_source = ?", filter.Trigger) + } + if filter.CreatedAtFrom != nil { + tx = tx.Where("created_at >= ?", filter.CreatedAtFrom.UTC()) + } + if filter.CreatedAtTo != nil { + tx = tx.Where("created_at <= ?", filter.CreatedAtTo.UTC()) + } + if includeStatus && filter.Status != "" { + tx = tx.Where("status = ?", filter.Status) + } + return tx +} + +func enrichTaskNames(tx *gorm.DB, values []po.RepositoryTask) error { + if len(values) == 0 { + return nil + } + repositoryIDs := make([]string, 0, len(values)) + nodeIDs := make([]string, 0, len(values)) + lookup := tx.Session(&gorm.Session{NewDB: true}) + for index := range values { + repositoryIDs = append(repositoryIDs, values[index].RepositoryID) + if values[index].StorageNodeID != "" { + nodeIDs = append(nodeIDs, values[index].StorageNodeID) + } + } + var repositories []po.SoftwareRepository + if err := lookup.Select("id", "name").Where("id IN ?", repositoryIDs).Find(&repositories).Error; err != nil { + return err + } + repositoryNames := make(map[string]string, len(repositories)) + for _, repository := range repositories { + repositoryNames[repository.ID] = repository.Name + } + nodeNames := make(map[string]string) + if len(nodeIDs) > 0 { + var nodes []po.RepositoryStorageNode + if err := lookup.Select("id", "name").Where("id IN ?", nodeIDs).Find(&nodes).Error; err != nil { + return err + } + for _, node := range nodes { + nodeNames[node.ID] = node.Name + } + } + for index := range values { + values[index].RepositoryName = repositoryNames[values[index].RepositoryID] + values[index].StorageNodeName = nodeNames[values[index].StorageNodeID] + } + return nil +} + +func (r *repository) FindClaimCandidate(now time.Time, storageNodeID string, + excludedRepositories []string, session entity.Session, +) ( + value *po.RepositoryTask, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + tx = tx.Clauses(clause.Locking{Strength: "UPDATE", Options: "SKIP LOCKED"}). + Where("status = ? AND available_at <= ? AND attempt < max_attempts AND executor_kind = ?", + request.RepositoryTaskStatusPending, now, "REPOSITORY_WORKER") + if storageNodeID != "" { + tx = tx.Where("storage_node_id = ?", storageNodeID) + } + if len(excludedRepositories) > 0 { + tx = tx.Where("NOT (repository_id IN ? AND requires_mutation_lock = ?)", excludedRepositories, true) + } + value = new(po.RepositoryTask) + if err = tx.Order("available_at ASC, created_at ASC, id ASC").Take(value).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + return value, nil +} + +func (r *repository) FindExpired(now time.Time, limit int, session entity.Session) ( + values []po.RepositoryTask, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + err = tx.Clauses(clause.Locking{Strength: "UPDATE", Options: "SKIP LOCKED"}). + Where("status IN ? AND lease_expires_at IS NOT NULL AND lease_expires_at <= ?", []string{ + request.RepositoryTaskStatusRunning, + request.RepositoryTaskStatusCancelRequested, + }, now).Order("lease_expires_at ASC, id ASC").Limit(limit).Find(&values).Error + return values, err +} diff --git a/manager/backend/services/footstone/domain/repository/repositorytaskrepo/retry_test.go b/manager/backend/services/footstone/domain/repository/repositorytaskrepo/retry_test.go new file mode 100644 index 0000000000000000000000000000000000000000..eddb50be786733d88677ebfe2875f70fc8c426f6 --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/repositorytaskrepo/retry_test.go @@ -0,0 +1,57 @@ +package repositorytaskrepo + +import ( + "errors" + "regexp" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/require" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +func TestFindUnfinishedRetryUsesCurrentReadAndAllUnfinishedStates(t *testing.T) { + repository, mock, db, sqlDB := newTaskStatisticsTestRepository(t) + defer sqlDB.Close() + query := regexp.QuoteMeta("SELECT * FROM `repository_tasks` WHERE repository_id = ? AND project_id = ? AND retry_of_task_id = ? AND task_type = ? AND status IN (?,?,?) ORDER BY created_at ASC, id ASC LIMIT 1 FOR UPDATE") + mock.ExpectQuery(query). + WithArgs("repository", int64(9), "original", request.RepositoryTaskTypeSync, + request.RepositoryTaskStatusPending, request.RepositoryTaskStatusRunning, + request.RepositoryTaskStatusCancelRequested). + WillReturnRows(sqlmock.NewRows([]string{"id", "retry_of_task_id", "status"}). + AddRow("existing-retry", "original", request.RepositoryTaskStatusCancelRequested)) + + value, err := repository.FindUnfinishedRetry(&po.RepositoryTask{ID: "original", RepositoryID: "repository", + ProjectID: 9, Type: request.RepositoryTaskTypeSync}, db) + require.NoError(t, err) + require.Equal(t, "existing-retry", value.ID) + require.Equal(t, request.RepositoryTaskStatusCancelRequested, value.Status) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestFindUnfinishedRetryReturnsNotFoundOrDatabaseFailure(t *testing.T) { + for _, databaseFailure := range []bool{false, true} { + t.Run(map[bool]string{false: "terminal_or_missing", true: "database_failure"}[databaseFailure], func(t *testing.T) { + repository, mock, db, sqlDB := newTaskStatisticsTestRepository(t) + defer sqlDB.Close() + query := mock.ExpectQuery(`(?s)SELECT .*repository_tasks.*retry_of_task_id.*FOR UPDATE`). + WithArgs("repository", int64(9), "original", request.RepositoryTaskTypeSync, + request.RepositoryTaskStatusPending, request.RepositoryTaskStatusRunning, + request.RepositoryTaskStatusCancelRequested) + expected := ErrTaskNotFound + if databaseFailure { + expected = errors.New("database unavailable") + query.WillReturnError(expected) + } else { + query.WillReturnRows(sqlmock.NewRows([]string{"id"})) + } + value, err := repository.FindUnfinishedRetry(&po.RepositoryTask{ID: "original", RepositoryID: "repository", + ProjectID: 9, Type: request.RepositoryTaskTypeSync}, db) + require.Nil(t, value) + require.ErrorIs(t, err, expected) + require.NoError(t, mock.ExpectationsWereMet()) + }) + } +} diff --git a/manager/backend/services/footstone/domain/repository/repositorytaskrepo/statistics_test.go b/manager/backend/services/footstone/domain/repository/repositorytaskrepo/statistics_test.go new file mode 100644 index 0000000000000000000000000000000000000000..e2d555d9395282abffdccb2250ae8c273e07e9fb --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/repositorytaskrepo/statistics_test.go @@ -0,0 +1,106 @@ +package repositorytaskrepo + +import ( + "database/sql" + "regexp" + "strings" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/require" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/baserepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +type taskStatisticsTestSessionFactory struct{ db *gorm.DB } + +func (f taskStatisticsTestSessionFactory) NewSession() entity.Session { return f.db.Begin() } +func (f taskStatisticsTestSessionFactory) DB() entity.Session { return f.db } + +func newTaskStatisticsTestRepository(t *testing.T) (*repository, sqlmock.Sqlmock, *gorm.DB, *sql.DB) { + t.Helper() + sqlDB, mock, err := sqlmock.New() + require.NoError(t, err) + db, err := gorm.Open(mysql.New(mysql.Config{ + Conn: sqlDB, SkipInitializeWithVersion: true, + }), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) + require.NoError(t, err) + return &repository{DBBaseRepository: baserepo.NewDBBaseRepository( + taskStatisticsTestSessionFactory{db: db}, nil, + )}, mock, db, sqlDB +} + +func TestTaskStatisticsIgnoreStatusButKeepAllOtherFilters(t *testing.T) { + repository, mock, db, sqlDB := newTaskStatisticsTestRepository(t) + from := time.Date(2026, time.September, 1, 1, 2, 3, 0, time.FixedZone("UTC+8", 8*60*60)) + to := from.Add(24 * time.Hour) + filter := request.ListRepositoryTasksRequest{ + ProjectID: 9, RepositoryID: "repo-1", StorageNodeID: "node-1", + Type: request.RepositoryTaskTypeSync, Status: request.RepositoryTaskStatusFailed, + Trigger: request.RepositoryTaskTriggerScheduled, CreatedAtFrom: &from, CreatedAtTo: &to, + } + + // The aggregate intentionally omits status while retaining every other + // active filter, so a FAILED list can still show the true four-state total. + mock.ExpectQuery(`(?s)SELECT .*pending_count.*FROM .*repository_tasks.*WHERE project_id = .*repository_id = .*storage_node_id = .*task_type = .*trigger_source = .*created_at >= .*created_at <= .*`). + WithArgs(int64(9), "repo-1", "node-1", request.RepositoryTaskTypeSync, + request.RepositoryTaskTriggerScheduled, from.UTC(), to.UTC()). + WillReturnRows(sqlmock.NewRows([]string{ + "pending_count", "running_count", "failed_count", "success_count", + }).AddRow(2, 3, 4, 5)) + + statistics, err := repository.Statistics(filter, db) + require.NoError(t, err) + require.Equal(t, request.RepositoryTaskStatistics{ + PendingCount: 2, RunningCount: 3, FailedCount: 4, SuccessCount: 5, + }, statistics) + require.NoError(t, mock.ExpectationsWereMet()) + mock.ExpectClose() + require.NoError(t, sqlDB.Close()) +} + +func TestTaskListAndStatisticsStatusFilterSeparation(t *testing.T) { + _, _, db, sqlDB := newTaskStatisticsTestRepository(t) + defer sqlDB.Close() + filter := request.ListRepositoryTasksRequest{ + ProjectID: 7, Status: request.RepositoryTaskStatusFailed, + } + + listStatement := applyTaskListFilters( + db.Session(&gorm.Session{DryRun: true}).Model(&po.RepositoryTask{}), filter, true, + ).Find(&[]po.RepositoryTask{}).Statement + statisticsStatement := applyTaskListFilters( + db.Session(&gorm.Session{DryRun: true}).Model(&po.RepositoryTask{}), filter, false, + ).Find(&[]po.RepositoryTask{}).Statement + + require.Contains(t, strings.ToLower(listStatement.SQL.String()), "status =") + require.NotContains(t, strings.ToLower(statisticsStatement.SQL.String()), "status =") + require.Equal(t, []interface{}{int64(7), request.RepositoryTaskStatusFailed}, listStatement.Vars) + require.Equal(t, []interface{}{int64(7)}, statisticsStatement.Vars) +} + +func TestTaskStatisticsFourStateSQLCoversPersistedTerminalStates(t *testing.T) { + repository, mock, db, sqlDB := newTaskStatisticsTestRepository(t) + mock.ExpectQuery(regexp.MustCompile(`(?s)SELECT .*status IN \('RUNNING', 'CANCEL_REQUESTED'\).*status IN \('FAILED', 'CANCELED'\).*status IN \('SUCCESS', 'PARTIAL_SUCCESS'\).*`).String()). + WithArgs(int64(1)). + WillReturnRows(sqlmock.NewRows([]string{ + "pending_count", "running_count", "failed_count", "success_count", + }).AddRow(1, 2, 3, 4)) + + statistics, err := repository.Statistics(request.ListRepositoryTasksRequest{ProjectID: 1}, db) + require.NoError(t, err) + require.EqualValues(t, 1, statistics.PendingCount) + require.EqualValues(t, 2, statistics.RunningCount) + require.EqualValues(t, 3, statistics.FailedCount) + require.EqualValues(t, 4, statistics.SuccessCount) + require.NoError(t, mock.ExpectationsWereMet()) + mock.ExpectClose() + require.NoError(t, sqlDB.Close()) +} diff --git a/manager/backend/services/footstone/domain/repository/repositorytaskresultrepo/repository.go b/manager/backend/services/footstone/domain/repository/repositorytaskresultrepo/repository.go new file mode 100644 index 0000000000000000000000000000000000000000..35f5d2252ddfedc58c229b823ef7ba8757deab17 --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/repositorytaskresultrepo/repository.go @@ -0,0 +1,69 @@ +package repositorytaskresultrepo + +import ( + "errors" + + "gorm.io/gorm" + "gorm.io/gorm/clause" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/baserepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +var ErrTaskResultItemConflict = errors.New("repository task result item conflicts with the stored result") + +type Repository interface { + SyncTables() error + CreateItems([]po.RepositoryTaskResultItem, entity.Session) error + ListByTaskID(string, entity.Session) ([]po.RepositoryTaskResultItem, error) +} + +type repository struct{ baserepo.DBBaseRepository } + +func New(client *database.DBClient) Repository { + return &repository{DBBaseRepository: baserepo.NewDBBaseRepository(client, + []interface{}{new(po.RepositoryTaskResultItem)})} +} + +// CreateItems is retry-safe: an identical task/item result is accepted, while +// a conflicting replay is rejected instead of silently rewriting audit data. +func (r *repository) CreateItems(values []po.RepositoryTaskResultItem, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + for index := range values { + value := &values[index] + result := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(value) + if result.Error != nil { + return result.Error + } + if result.RowsAffected != 0 { + continue + } + var stored po.RepositoryTaskResultItem + if err = tx.Where("task_id = ? AND item_key = ?", value.TaskID, value.ItemKey).Take(&stored).Error; err != nil { + return err + } + if stored.PackageNEVRA != value.PackageNEVRA || stored.Status != value.Status || + stored.ErrorCode != value.ErrorCode || stored.Message != value.Message || + string(stored.ResultJSON) != string(value.ResultJSON) { + return ErrTaskResultItemConflict + } + } + return nil +} + +func (r *repository) ListByTaskID(taskID string, session entity.Session) ( + values []po.RepositoryTaskResultItem, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + err = tx.Where("task_id = ?", taskID). + Order("CASE WHEN status IN ('FAILED', 'CANCELED') THEN 0 ELSE 1 END, item_key ASC"). + Find(&values).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return []po.RepositoryTaskResultItem{}, nil + } + return values, err +} diff --git a/manager/backend/services/footstone/domain/repository/repositoryupstreamprofilerepo/repository.go b/manager/backend/services/footstone/domain/repository/repositoryupstreamprofilerepo/repository.go new file mode 100644 index 0000000000000000000000000000000000000000..423901589874d85ca737b5e681640eaeda4990cf --- /dev/null +++ b/manager/backend/services/footstone/domain/repository/repositoryupstreamprofilerepo/repository.go @@ -0,0 +1,123 @@ +package repositoryupstreamprofilerepo + +import ( + "errors" + "fmt" + + "github.com/go-sql-driver/mysql" + "gorm.io/gorm" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/baserepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +var ( + ErrUpstreamProfileNotFound = errors.New("repository upstream profile not found") + ErrUpstreamProfileDuplicate = errors.New("repository upstream profile already exists in this project") +) + +type Repository interface { + SyncTables() error + Create(*po.RepositoryUpstreamProfile, entity.Session) error + Update(*po.RepositoryUpstreamProfile, entity.Session) error + GetByID(string, entity.Session) (*po.RepositoryUpstreamProfile, error) + List(request.ListRepositoryUpstreamProfilesRequest, entity.Session) ([]po.RepositoryUpstreamProfile, request.PageResponse, error) + Delete(string, entity.Session) error +} + +type repository struct{ baserepo.DBBaseRepository } + +func New(client *database.DBClient) Repository { + return &repository{DBBaseRepository: baserepo.NewDBBaseRepository(client, []interface{}{new(po.RepositoryUpstreamProfile)})} +} + +func (r *repository) Create(value *po.RepositoryUpstreamProfile, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + if err = tx.Create(value).Error; err != nil { + return normalizeWriteError(err) + } + return nil +} + +func (r *repository) Update(value *po.RepositoryUpstreamProfile, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + result := tx.Model(&po.RepositoryUpstreamProfile{}).Where("id = ?", value.ID).Updates(map[string]interface{}{ + "name": value.Name, "description": value.Description, "system_product": value.SystemProduct, + "channel": value.Channel, "url_template": value.URLTemplate, + "supported_versions": value.SupportedVersionsJSON, + "supported_architectures": value.SupportedArchitecturesJSON, + "default_include_packages": value.DefaultIncludePackagesJSON, + "default_exclude_packages": value.DefaultExcludePackagesJSON, "enabled": value.Enabled, + }) + if result.Error != nil { + return normalizeWriteError(result.Error) + } + if result.RowsAffected == 0 { + return ErrUpstreamProfileNotFound + } + return nil +} + +func (r *repository) GetByID(id string, session entity.Session) (_ *po.RepositoryUpstreamProfile, err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + value := new(po.RepositoryUpstreamProfile) + if err = tx.Where("id = ?", id).Take(value).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrUpstreamProfileNotFound + } + return nil, err + } + return value, nil +} + +func (r *repository) List(filter request.ListRepositoryUpstreamProfilesRequest, session entity.Session) ( + values []po.RepositoryUpstreamProfile, page request.PageResponse, err error, +) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + tx = tx.Model(&po.RepositoryUpstreamProfile{}).Where("project_id = ?", filter.ProjectID) + if filter.SystemProduct != "" { + tx = tx.Where("system_product = ?", filter.SystemProduct) + } + if filter.Enabled != nil { + tx = tx.Where("enabled = ?", *filter.Enabled) + } + if filter.Keyword != "" { + keyword := "%" + filter.Keyword + "%" + tx = tx.Where("name LIKE ? OR description LIKE ? OR channel LIKE ?", keyword, keyword, keyword) + } + var count int64 + if err = tx.Count(&count).Error; err != nil { + return nil, page, err + } + limit, offset := filter.PageRequest.Limit() + err = tx.Order("system_product ASC, channel ASC, name ASC").Offset(offset).Limit(limit).Find(&values).Error + return values, filter.PageRequest.Response(count), err +} + +func (r *repository) Delete(id string, session entity.Session) (err error) { + tx, maker := r.NewSessionMaker(session) + defer maker.Close(&err) + result := tx.Where("id = ?", id).Delete(&po.RepositoryUpstreamProfile{}) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return ErrUpstreamProfileNotFound + } + return nil +} + +func normalizeWriteError(err error) error { + var mysqlErr *mysql.MySQLError + if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 { + return ErrUpstreamProfileDuplicate + } + return fmt.Errorf("write repository upstream profile: %w", err) +} diff --git a/manager/backend/services/footstone/domain/repositoryAIService/service.go b/manager/backend/services/footstone/domain/repositoryAIService/service.go new file mode 100644 index 0000000000000000000000000000000000000000..92ca9eaa2c36a2ac074de0ca27f2bc4f01ea1b47 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryAIService/service.go @@ -0,0 +1,89 @@ +package repositoryAIService + +import ( + "errors" + "regexp" + "strings" + "time" + + "github.com/google/uuid" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositoryairepo" +) + +const ( + StatusCreated = "CREATED" + StatusFeedbackRecorded = "FEEDBACK_RECORDED" +) + +var ( + ErrUnauthenticated = errors.New("authenticated user is required") + ErrContextHashInvalid = errors.New("repository OCAI context SHA-256 is invalid") + ErrFeedbackInvalid = errors.New("repository OCAI feedback is invalid") + sha256Pattern = regexp.MustCompile(`^[0-9a-f]{64}$`) + resultPattern = regexp.MustCompile(`^[A-Z][A-Z0-9_]{0,31}$`) +) + +type Service = *service + +type service struct { + analyses repositoryairepo.Repository + now func() time.Time +} + +func New(db *database.DBClient) Service { + return newService(repositoryairepo.New(db)) +} + +func newService(analyses repositoryairepo.Repository) Service { + return &service{analyses: analyses, now: func() time.Time { return time.Now().UTC() }} +} + +func (s *service) SyncTables() error { return s.analyses.SyncTables() } + +func (s *service) Create(ctx entity.Context, taskID, contextSHA256 string) ( + value *po.RepositoryAIAnalysis, err error, +) { + if strings.TrimSpace(ctx.User.LoginName) == "" { + return nil, ErrUnauthenticated + } + contextSHA256 = strings.ToLower(strings.TrimSpace(contextSHA256)) + if !sha256Pattern.MatchString(contextSHA256) { + return nil, ErrContextHashInvalid + } + value = &po.RepositoryAIAnalysis{ + ID: uuid.NewString(), TaskID: strings.TrimSpace(taskID), + SessionID: "repo_" + strings.ReplaceAll(uuid.NewString(), "-", ""), + ContextSHA256: contextSHA256, Status: StatusCreated, + RequestedBy: ctx.User.LoginName, CreatedAt: s.now(), + } + if err = s.analyses.Create(value, nil); err != nil { + return nil, err + } + return value, nil +} + +func (s *service) Describe(id string) (*po.RepositoryAIAnalysis, error) { + return s.analyses.GetByID(strings.TrimSpace(id), nil) +} + +func (s *service) DescribeBySessionID(id string) (*po.RepositoryAIAnalysis, error) { + return s.analyses.GetBySessionID(strings.TrimSpace(id), nil) +} + +func (s *service) AppendFeedback(sessionID, actionTaskID, actionResult string) ( + *po.RepositoryAIAnalysis, error, +) { + sessionID, actionTaskID = strings.TrimSpace(sessionID), strings.TrimSpace(actionTaskID) + actionResult = strings.ToUpper(strings.TrimSpace(actionResult)) + if sessionID == "" || actionTaskID == "" || !resultPattern.MatchString(actionResult) { + return nil, ErrFeedbackInvalid + } + if err := s.analyses.UpdateFeedback(sessionID, actionTaskID, actionResult, nil); err != nil { + return nil, err + } + return s.analyses.GetBySessionID(sessionID, nil) +} diff --git a/manager/backend/services/footstone/domain/repositoryAIService/service_test.go b/manager/backend/services/footstone/domain/repositoryAIService/service_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d2512a0faf8461319251bafb7596efb8d58f336e --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryAIService/service_test.go @@ -0,0 +1,94 @@ +package repositoryAIService + +import ( + "testing" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + ftpb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/footstone" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +type analysisMemoryRepository struct { + values map[string]*po.RepositoryAIAnalysis +} + +func (r *analysisMemoryRepository) SyncTables() error { return nil } + +func (r *analysisMemoryRepository) Create(value *po.RepositoryAIAnalysis, _ entity.Session) error { + copyValue := *value + r.values[value.ID] = ©Value + return nil +} + +func (r *analysisMemoryRepository) GetByID(id string, _ entity.Session) (*po.RepositoryAIAnalysis, error) { + for _, value := range r.values { + if value.ID == id { + copyValue := *value + return ©Value, nil + } + } + return nil, errAnalysisNotFoundForTest +} + +func (r *analysisMemoryRepository) GetBySessionID(id string, _ entity.Session) (*po.RepositoryAIAnalysis, error) { + for _, value := range r.values { + if value.SessionID == id { + copyValue := *value + return ©Value, nil + } + } + return nil, errAnalysisNotFoundForTest +} + +func (r *analysisMemoryRepository) UpdateFeedback(sessionID, actionTaskID, actionResult string, _ entity.Session) error { + for _, value := range r.values { + if value.SessionID == sessionID { + value.Status = StatusFeedbackRecorded + value.ActionTaskID = actionTaskID + value.ActionResult = actionResult + now := time.Unix(200, 0).UTC() + value.FeedbackAt = &now + return nil + } + } + return errAnalysisNotFoundForTest +} + +var errAnalysisNotFoundForTest = &analysisTestError{} + +type analysisTestError struct{} + +func (*analysisTestError) Error() string { return "analysis not found" } + +func TestCreateAndAppendFeedback(t *testing.T) { + repository := &analysisMemoryRepository{values: map[string]*po.RepositoryAIAnalysis{}} + service := newService(repository) + service.now = func() time.Time { return time.Unix(100, 0).UTC() } + ctx := entity.NewDefaultContext() + ctx.User = &ftpb.UserInfo{LoginName: "repository-admin"} + value, err := service.Create(ctx, "4ea886c7-2240-4ca9-bdb5-9b76e80d91a6", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + if err != nil { + t.Fatalf("create analysis: %v", err) + } + if len(value.SessionID) != 37 || value.SessionID[:5] != "repo_" { + t.Fatalf("unexpected OCAI session id %q", value.SessionID) + } + updated, err := service.AppendFeedback(value.SessionID, "d91e803e-436d-4d25-a54c-d206864d1a13", "success") + if err != nil { + t.Fatalf("append feedback: %v", err) + } + if updated.ActionResult != "SUCCESS" || updated.Status != StatusFeedbackRecorded || updated.FeedbackAt == nil { + t.Fatalf("feedback was not persisted: %#v", updated) + } +} + +func TestCreateRejectsInvalidContextHash(t *testing.T) { + service := newService(&analysisMemoryRepository{values: map[string]*po.RepositoryAIAnalysis{}}) + ctx := entity.NewDefaultContext() + ctx.User = &ftpb.UserInfo{LoginName: "repository-admin"} + if _, err := service.Create(ctx, "task", "not-a-digest"); err != ErrContextHashInvalid { + t.Fatalf("expected context hash rejection, got %v", err) + } +} diff --git a/manager/backend/services/footstone/domain/repositoryContentService/gpg_key.go b/manager/backend/services/footstone/domain/repositoryContentService/gpg_key.go new file mode 100644 index 0000000000000000000000000000000000000000..cb3931f4a93672bdb4e43ee60d5666c20fdc75de --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryContentService/gpg_key.go @@ -0,0 +1,255 @@ +package repositoryContentService + +import ( + "bytes" + "encoding/hex" + "errors" + "fmt" + "strings" + + "github.com/google/uuid" + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/armor" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +const maxRepositoryPublicKeyBytes = 1 << 20 +const maxRepositoryGPGKeys = 64 + +var ( + ErrInvalidGPGPublicKey = errors.New("invalid OpenPGP public key") + ErrMultipleGPGKeys = errors.New("exactly one OpenPGP public key is required") + ErrGPGKeyDeleteConfirm = errors.New("repository GPG key deletion requires explicit confirmation") + ErrGPGKeyInUse = errors.New("repository GPG key cannot be deleted while the active revision contains packages") +) + +func (s *service) RegisterGPGKey(ctx entity.Context, repositoryID string, publicKey []byte) ( + result *request.RepositoryGPGKey, err error, +) { + repository, err := s.repositories.Describe(ctx, strings.TrimSpace(repositoryID)) + if err != nil { + return nil, err + } + existingKeys, err := s.content.ListGPGKeys(repository.ID, nil) + if err != nil { + return nil, err + } + if len(existingKeys) >= maxRepositoryGPGKeys { + return nil, fmt.Errorf("repository GPG key count cannot exceed %d", maxRepositoryGPGKeys) + } + fingerprint, keyID, normalizedPublicKey, err := inspectGPGPublicKey(publicKey) + if err != nil { + return nil, err + } + relative, err := s.store.GPGKeyRelativePath(repository.ID, fingerprint) + if err != nil { + return nil, err + } + value := &po.RepositoryGPGKey{ + ID: uuid.NewString(), RepositoryID: repository.ID, KeyID: keyID, + Fingerprint: fingerprint, PublicKeyPath: relative, + } + stored := false + var node *po.RepositoryStorageNode + if s.remoteUpload != nil { + node, err = s.remoteNode(repository.PrimaryStorageNodeID) + if err != nil { + return nil, err + } + } + tx, maker := entity.NewSessionMaker(nil, s.db) + defer func() { + if err != nil && stored { + if s.remoteUpload != nil { + _ = s.remoteUpload.RemoveGPGPublicKey(ctx.Context, node, repository.ID, + repository.PublishKey, fingerprint) + } else { + _ = s.store.RemoveGPGPublicKey(repository.ID, repository.PublishKey, fingerprint) + } + } + }() + defer maker.Close(&err) + if err = s.content.CreateGPGKey(value, tx); err != nil { + return nil, err + } + publicURL := "" + if s.remoteUpload != nil { + value.PublicKeyPath, publicURL, err = s.remoteUpload.PutGPGPublicKey(ctx.Context, node, + repository.ID, repository.PublishKey, fingerprint, keyID, normalizedPublicKey) + stored = err == nil + } else { + value.PublicKeyPath, err = s.store.PutGPGPublicKey(repository.ID, repository.PublishKey, + fingerprint, normalizedPublicKey) + if err == nil { + stored = true + publicURL, err = s.store.PublicGPGKeyURL(repository.PublishKey, fingerprint) + } + } + if err != nil { + return nil, err + } + return gpgKeyView(value, publicURL), nil +} + +func (s *service) DeleteGPGKey(ctx entity.Context, repositoryID, keyID string, confirm bool) (err error) { + if !confirm { + return ErrGPGKeyDeleteConfirm + } + repository, err := s.repositories.Describe(ctx, strings.TrimSpace(repositoryID)) + if err != nil { + return err + } + if repository.ActiveRevisionID != "" { + hasPackages, listErr := s.content.HasPackagesByRevision(repository.ActiveRevisionID, nil) + if listErr != nil { + return listErr + } + if hasPackages { + return ErrGPGKeyInUse + } + } + if _, err = uuid.Parse(strings.TrimSpace(keyID)); err != nil { + return fmt.Errorf("invalid key_id: %w", err) + } + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + key, err := s.content.GetGPGKey(repository.ID, keyID, tx) + if err != nil { + return err + } + if err = s.content.DeleteGPGKey(key.ID, tx); err != nil { + return err + } + if s.remoteUpload != nil { + node, nodeErr := s.remoteNode(repository.PrimaryStorageNodeID) + if nodeErr != nil { + return nodeErr + } + err = s.remoteUpload.RemoveGPGPublicKey(ctx.Context, node, repository.ID, + repository.PublishKey, key.Fingerprint) + } else { + err = s.store.RemoveGPGPublicKey(repository.ID, repository.PublishKey, key.Fingerprint) + } + return err +} + +func (s *service) ListGPGKeys(ctx entity.Context, repositoryID string) ([]request.RepositoryGPGKey, error) { + repository, err := s.repositories.Describe(ctx, strings.TrimSpace(repositoryID)) + if err != nil { + return nil, err + } + values, err := s.content.ListGPGKeys(repository.ID, nil) + if err != nil { + return nil, err + } + result := make([]request.RepositoryGPGKey, 0, len(values)) + var node *po.RepositoryStorageNode + if s.remoteUpload != nil { + node, err = s.remoteNode(repository.PrimaryStorageNodeID) + if err != nil { + return nil, err + } + } + for index := range values { + value := &values[index] + publicURL := "" + if s.remoteUpload != nil { + publicURL, err = s.remoteUpload.VerifyGPGPublicKey(ctx.Context, node, repository.ID, + repository.PublishKey, value.Fingerprint, value.PublicKeyPath) + } else { + err = s.store.VerifyGPGPublicKey(repository.ID, repository.PublishKey, + value.Fingerprint, value.PublicKeyPath) + if err == nil { + publicURL, err = s.store.PublicGPGKeyURL(repository.PublishKey, value.Fingerprint) + } + } + if err != nil { + return nil, fmt.Errorf("verify repository GPG key %s: %w", value.Fingerprint, err) + } + result = append(result, *gpgKeyView(value, publicURL)) + } + return result, nil +} + +// ListGPGKeyMetadataInternal returns the immutable key references embedded in a node task. +// The repository worker independently verifies the referenced files before importing them. +func (s *service) ListGPGKeyMetadataInternal(repositoryID string) ([]po.RepositoryGPGKey, error) { + if _, err := uuid.Parse(strings.TrimSpace(repositoryID)); err != nil { + return nil, fmt.Errorf("invalid repository_id: %w", err) + } + return s.content.ListGPGKeys(strings.TrimSpace(repositoryID), nil) +} + +func gpgKeyView(value *po.RepositoryGPGKey, publicURL string) *request.RepositoryGPGKey { + return &request.RepositoryGPGKey{ + ID: value.ID, RepositoryID: value.RepositoryID, KeyID: value.KeyID, + Fingerprint: value.Fingerprint, PublicKeyPath: value.PublicKeyPath, + PublicKeyURL: publicURL, CreatedAt: value.CreatedAt, + } +} + +func inspectGPGPublicKey(content []byte) (string, string, []byte, error) { + if len(content) == 0 || len(content) > maxRepositoryPublicKeyBytes { + return "", "", nil, fmt.Errorf("%w: size must be between 1 and %d bytes", + ErrInvalidGPGPublicKey, maxRepositoryPublicKeyBytes) + } + if bytes.Contains(content, []byte("-----BEGIN PGP PRIVATE KEY BLOCK-----")) { + return "", "", nil, fmt.Errorf("%w: private key material is forbidden", ErrInvalidGPGPublicKey) + } + if countOpenPGPArmorBlocks(content) != 1 { + return "", "", nil, ErrMultipleGPGKeys + } + block, err := armor.Decode(bytes.NewReader(content)) + if err != nil || block.Type != openpgp.PublicKeyType { + return "", "", nil, fmt.Errorf("%w: an ASCII-armored public key block is required", ErrInvalidGPGPublicKey) + } + entities, err := openpgp.ReadKeyRing(block.Body) + if err != nil { + return "", "", nil, fmt.Errorf("%w: %v", ErrInvalidGPGPublicKey, err) + } + if len(entities) != 1 || entities[0] == nil || entities[0].PrimaryKey == nil { + return "", "", nil, ErrMultipleGPGKeys + } + value := entities[0] + if value.PrivateKey != nil { + return "", "", nil, fmt.Errorf("%w: private key material is forbidden", ErrInvalidGPGPublicKey) + } + for _, subkey := range value.Subkeys { + if subkey.PrivateKey != nil { + return "", "", nil, fmt.Errorf("%w: private subkey material is forbidden", ErrInvalidGPGPublicKey) + } + } + fingerprint := strings.ToUpper(hex.EncodeToString(value.PrimaryKey.Fingerprint[:])) + keyID := strings.ToUpper(value.PrimaryKey.KeyIdString()) + if fingerprint == "" || keyID == "" { + return "", "", nil, ErrInvalidGPGPublicKey + } + var normalized bytes.Buffer + writer, err := armor.Encode(&normalized, openpgp.PublicKeyType, nil) + if err != nil { + return "", "", nil, fmt.Errorf("%w: encode public key: %v", ErrInvalidGPGPublicKey, err) + } + if err = value.Serialize(writer); err == nil { + err = writer.Close() + } else { + _ = writer.Close() + } + if err != nil { + return "", "", nil, fmt.Errorf("%w: serialize public key: %v", ErrInvalidGPGPublicKey, err) + } + return fingerprint, keyID, normalized.Bytes(), nil +} + +func countOpenPGPArmorBlocks(content []byte) int { + count := 0 + for _, line := range bytes.Split(content, []byte{'\n'}) { + line = bytes.TrimSpace(line) + if bytes.HasPrefix(line, []byte("-----BEGIN PGP ")) && bytes.HasSuffix(line, []byte("-----")) { + count++ + } + } + return count +} diff --git a/manager/backend/services/footstone/domain/repositoryContentService/gpg_key_test.go b/manager/backend/services/footstone/domain/repositoryContentService/gpg_key_test.go new file mode 100644 index 0000000000000000000000000000000000000000..2fbcbfe2ff3b755a2fc2c6a93d824133c0297247 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryContentService/gpg_key_test.go @@ -0,0 +1,105 @@ +package repositoryContentService + +import ( + "bytes" + "os" + "testing" + + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/armor" +) + +func TestInspectGPGPublicKey(t *testing.T) { + entity, err := openpgp.NewEntity("OCManager Test", "", "repository@example.invalid", nil) + if err != nil { + t.Fatal(err) + } + var content bytes.Buffer + writer, err := armor.Encode(&content, openpgp.PublicKeyType, nil) + if err != nil { + t.Fatal(err) + } + if err = entity.Serialize(writer); err != nil { + t.Fatal(err) + } + if err = writer.Close(); err != nil { + t.Fatal(err) + } + fingerprint, keyID, normalized, err := inspectGPGPublicKey(content.Bytes()) + if err != nil || len(fingerprint) != 40 || len(keyID) != 16 { + t.Fatalf("fingerprint=%q keyID=%q err=%v", fingerprint, keyID, err) + } + if !bytes.Contains(normalized, []byte("BEGIN PGP PUBLIC KEY BLOCK")) { + t.Fatal("normalized public key was not returned") + } +} + +func TestInspectGPGPublicKeyRejectsPrivateMaterial(t *testing.T) { + entity, err := openpgp.NewEntity("OCManager Test", "", "repository@example.invalid", nil) + if err != nil { + t.Fatal(err) + } + var content bytes.Buffer + writer, err := armor.Encode(&content, openpgp.PrivateKeyType, nil) + if err != nil { + t.Fatal(err) + } + if err = entity.SerializePrivate(writer, nil); err != nil { + t.Fatal(err) + } + if err = writer.Close(); err != nil { + t.Fatal(err) + } + if _, _, _, err = inspectGPGPublicKey(content.Bytes()); err == nil { + t.Fatal("private OpenPGP material was accepted") + } +} + +func TestInspectGPGPublicKeyRejectsTrailingPrivateMaterial(t *testing.T) { + entity, err := openpgp.NewEntity("OCManager Test", "", "repository@example.invalid", nil) + if err != nil { + t.Fatal(err) + } + var public bytes.Buffer + publicWriter, err := armor.Encode(&public, openpgp.PublicKeyType, nil) + if err != nil { + t.Fatal(err) + } + if err = entity.Serialize(publicWriter); err != nil { + t.Fatal(err) + } + if err = publicWriter.Close(); err != nil { + t.Fatal(err) + } + var private bytes.Buffer + privateWriter, err := armor.Encode(&private, openpgp.PrivateKeyType, nil) + if err != nil { + t.Fatal(err) + } + if err = entity.SerializePrivate(privateWriter, nil); err != nil { + t.Fatal(err) + } + if err = privateWriter.Close(); err != nil { + t.Fatal(err) + } + combined := append(append([]byte{}, public.Bytes()...), private.Bytes()...) + if _, _, _, err = inspectGPGPublicKey(combined); err == nil { + t.Fatal("trailing private OpenPGP material was accepted") + } +} + +func TestInspectGPGPublicKeyOC9Fixture(t *testing.T) { + path := os.Getenv("OCM_REPOSITORY_GPG_PUBLIC_KEY") + if path == "" { + t.Skip("OC9 GPG public key fixture is not configured") + } + content, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + fingerprint, keyID, _, err := inspectGPGPublicKey(content) + if err != nil || fingerprint == "" || keyID == "" { + t.Fatalf("fingerprint=%q keyID=%q err=%v", fingerprint, keyID, err) + } + t.Logf("fingerprint=%s key_id=%s", fingerprint, keyID) +} diff --git a/manager/backend/services/footstone/domain/repositoryContentService/internal/admin.go b/manager/backend/services/footstone/domain/repositoryContentService/internal/admin.go new file mode 100644 index 0000000000000000000000000000000000000000..bcca9245aafc221c45ae528902ac49bec68ede36 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryContentService/internal/admin.go @@ -0,0 +1,18 @@ +package internal + +import ( + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" +) + +func IsGlobalAdministrator(ctx entity.Context) bool { + if ctx.User == nil { + return false + } + for _, role := range ctx.User.Roles { + if role.RoleId == request.DefaultGlobalRoleName { + return true + } + } + return false +} diff --git a/manager/backend/services/footstone/domain/repositoryContentService/migration.go b/manager/backend/services/footstone/domain/repositoryContentService/migration.go new file mode 100644 index 0000000000000000000000000000000000000000..166e40e909e2a6aa120fabeb8f8e60c36b2cb204 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryContentService/migration.go @@ -0,0 +1,300 @@ +package repositoryContentService + +import ( + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "net/url" + "path" + "strings" + + "github.com/google/uuid" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +var ( + ErrRepositoryMigrationActiveTask = errors.New("repository has an unfinished mutation task") + ErrRepositoryMigrationNoContent = errors.New("repository has no active revision; update its storage node directly") + ErrRepositoryMigrationSameNode = errors.New("repository is already assigned to the target storage node") + ErrRepositoryMigrationSource = errors.New("repository active revision is not a migratable immutable snapshot") +) + +func (s *service) MigrateRepository(ctx entity.Context, repositoryID, targetStorageNodeID string) ( + task *po.RepositoryTask, err error, +) { + repository, err := s.repositories.Describe(ctx, strings.TrimSpace(repositoryID)) + if err != nil { + return nil, err + } + targetStorageNodeID = strings.TrimSpace(targetStorageNodeID) + if uuid.Validate(targetStorageNodeID) != nil { + return nil, fmt.Errorf("target_storage_node_id must be a UUID") + } + if repository.PrimaryStorageNodeID == targetStorageNodeID { + return nil, ErrRepositoryMigrationSameNode + } + target, err := s.remoteNode(targetStorageNodeID) + if err != nil { + return nil, err + } + if target.ProjectID != repository.ProjectID { + return nil, errors.New("target storage node belongs to another project") + } + if uuid.Validate(repository.ActiveRevisionID) != nil { + return nil, ErrRepositoryMigrationNoContent + } + revision, err := s.content.GetRevision(repository.ID, repository.ActiveRevisionID, nil) + if err != nil { + return nil, err + } + if revision.Status != request.RepositoryRevisionStatusPublished || revision.PublishedAt == nil || + revision.StorageNodeID != repository.PrimaryStorageNodeID || revision.PackageCount < 0 || + revision.TotalBytes < 0 || !validMigrationDigest(revision.ManifestSHA256) || + !validMigrationDigest(revision.RepomdSHA256) || !validMigrationDigest(revision.SnapshotManifestSHA256) { + return nil, ErrRepositoryMigrationSource + } + packages, err := s.content.PackagesByRevision(revision.ID, nil) + if err != nil { + return nil, err + } + var totalBytes int64 + for _, item := range packages { + if item.RepositoryID != repository.ID || item.RevisionID != revision.ID || + item.SizeBytes <= 0 || !validMigrationDigest(item.SHA256) { + return nil, ErrRepositoryMigrationSource + } + totalBytes += item.SizeBytes + } + if len(packages) != revision.PackageCount || totalBytes != revision.TotalBytes { + return nil, ErrRepositoryMigrationSource + } + gpgKeys, err := s.content.ListGPGKeys(repository.ID, nil) + if err != nil { + return nil, err + } + migrationGPGKeys := make([]request.RepositoryMigrationGPGKey, 0, len(gpgKeys)) + for _, key := range gpgKeys { + publicURL, publicErr := migrationPublicKeyURL(repository.PublishBaseURLSnapshot, + repository.PublishKey, key.Fingerprint) + if publicErr != nil { + return nil, fmt.Errorf("%w: invalid GPG public key URL: %v", ErrRepositoryMigrationSource, publicErr) + } + migrationGPGKeys = append(migrationGPGKeys, request.RepositoryMigrationGPGKey{ + KeyID: key.KeyID, Fingerprint: key.Fingerprint, + PublicKeyPath: key.PublicKeyPath, SourcePublicURL: publicURL, + }) + } + sourceURL, err := immutableMigrationURL(repository.PublishBaseURLSnapshot, + repository.PublishKey, revision.ID) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrRepositoryMigrationSource, err) + } + snapshot := request.RepositoryMigrationSnapshot{ + SourceStorageNodeID: repository.PrimaryStorageNodeID, TargetStorageNodeID: target.ID, + TargetPublishBaseURL: target.PublishBaseURL, + SourceRevisionURL: sourceURL, RevisionID: revision.ID, RevisionKind: revision.Kind, + ManifestSHA256: revision.ManifestSHA256, RepomdSHA256: revision.RepomdSHA256, + SnapshotManifestSHA256: revision.SnapshotManifestSHA256, + PackageCount: revision.PackageCount, TotalBytes: revision.TotalBytes, + GPGKeys: migrationGPGKeys, + } + payload, err := json.Marshal(snapshot) + if err != nil { + return nil, err + } + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + locked, err := s.repositoryRecords.GetByID(repository.ID, tx) + if err != nil { + return nil, err + } + if locked.ActiveRevisionID != repository.ActiveRevisionID || + locked.PrimaryStorageNodeID != repository.PrimaryStorageNodeID || + locked.PublishBaseURLSnapshot != repository.PublishBaseURLSnapshot { + return nil, errors.New("repository changed while preparing migration") + } + unfinished, err := s.tasks.HasUnfinishedMutation(repository.ID, tx) + if err != nil { + return nil, err + } + if unfinished { + return nil, ErrRepositoryMigrationActiveTask + } + if err = s.repositoryRecords.UpdateStatus(repository.ID, request.RepositoryStatusMigrationRequired, tx); err != nil { + return nil, err + } + return s.tasks.EnqueueMigrationInSession(ctx, request.EnqueueRepositoryTaskRequest{ + RepositoryID: repository.ID, RevisionID: revision.ID, + Type: request.RepositoryTaskTypeMigrateRepository, PayloadJSON: string(payload), MaxAttempts: 3, + }, target.ID, tx) +} + +// ValidateRemoteMigration proves that a target-node result is an exact copy of +// the immutable revision already recorded by Footstone. It intentionally does +// not create a second revision or package inventory. +func (s *service) ValidateRemoteMigration(task *po.RepositoryTask, snapshot request.RepositoryMigrationSnapshot, + result *po.RepositoryRevision, packages []po.RepositoryPackage, session entity.Session, +) error { + if task == nil || result == nil || session == nil || + task.Type != request.RepositoryTaskTypeMigrateRepository { + return errors.New("migration task, result, and transaction are required") + } + target, err := s.remoteNode(snapshot.TargetStorageNodeID) + if err != nil { + return err + } + if target.PublishBaseURL != snapshot.TargetPublishBaseURL { + return errors.New("migration target publish URL changed after the task was created") + } + repository, err := s.repositoryRecords.GetByID(task.RepositoryID, session) + if err != nil { + return err + } + if repository.Status != request.RepositoryStatusMigrationRequired || + repository.ActiveRevisionID != snapshot.RevisionID || + repository.PrimaryStorageNodeID != snapshot.SourceStorageNodeID || + task.RevisionID != snapshot.RevisionID || task.StorageNodeID != snapshot.TargetStorageNodeID { + return errors.New("repository migration state no longer matches the frozen task") + } + stored, err := s.content.GetRevision(repository.ID, snapshot.RevisionID, session) + if err != nil { + return err + } + expectedRelative := path.Join("public", repository.PublishKey, "revisions", snapshot.RevisionID) + if stored.StorageNodeID != snapshot.SourceStorageNodeID || stored.Status != request.RepositoryRevisionStatusPublished || + stored.Kind != snapshot.RevisionKind || stored.ManifestSHA256 != snapshot.ManifestSHA256 || + stored.RepomdSHA256 != snapshot.RepomdSHA256 || + stored.SnapshotManifestSHA256 != snapshot.SnapshotManifestSHA256 || + stored.PackageCount != snapshot.PackageCount || stored.TotalBytes != snapshot.TotalBytes || + stored.RelativePath != expectedRelative { + return errors.New("source revision no longer matches the frozen migration snapshot") + } + if result.ID != stored.ID || result.RepositoryID != stored.RepositoryID || + result.StorageNodeID != snapshot.TargetStorageNodeID || result.TaskID != task.ID || + result.Kind != stored.Kind || result.Status != request.RepositoryRevisionStatusPublished || + result.PackageCount != stored.PackageCount || result.TotalBytes != stored.TotalBytes || + result.ManifestSHA256 != stored.ManifestSHA256 || result.RepomdSHA256 != stored.RepomdSHA256 || + result.SnapshotManifestSHA256 != stored.SnapshotManifestSHA256 || result.RelativePath != expectedRelative { + return errors.New("target revision result differs from the immutable source revision") + } + storedPackages, err := s.content.PackagesByRevision(stored.ID, session) + if err != nil { + return err + } + if !sameRemotePackages(storedPackages, packages) { + return errors.New("target package inventory differs from the immutable source revision") + } + keys, err := s.content.ListGPGKeys(repository.ID, session) + if err != nil { + return err + } + if len(keys) != len(snapshot.GPGKeys) { + return errors.New("repository GPG key set changed after migration was requested") + } + for index, key := range keys { + frozen := snapshot.GPGKeys[index] + publicURL, publicErr := migrationPublicKeyURL(repository.PublishBaseURLSnapshot, + repository.PublishKey, key.Fingerprint) + if publicErr != nil || key.KeyID != frozen.KeyID || key.Fingerprint != frozen.Fingerprint || + key.PublicKeyPath != frozen.PublicKeyPath || publicURL != frozen.SourcePublicURL { + return errors.New("repository GPG key set changed after migration was requested") + } + } + return nil +} + +// CompleteRemoteMigration binds the existing active revision to its target +// node and updates the repository publication URL in the task transaction. +func (s *service) CompleteRemoteMigration(task *po.RepositoryTask, + snapshot request.RepositoryMigrationSnapshot, session entity.Session, +) error { + if task == nil || session == nil || task.Type != request.RepositoryTaskTypeMigrateRepository || + task.PreparedRevisionID != snapshot.RevisionID || task.StorageNodeID != snapshot.TargetStorageNodeID { + return errors.New("migration confirmation does not match the prepared task") + } + repository, err := s.repositoryRecords.GetByID(task.RepositoryID, session) + if err != nil { + return err + } + if repository.ActiveRevisionID != snapshot.RevisionID { + return errors.New("repository active revision changed during migration") + } + if repository.PrimaryStorageNodeID == snapshot.TargetStorageNodeID && + repository.PublishBaseURLSnapshot == snapshot.TargetPublishBaseURL && + repository.Status == request.RepositoryStatusReady { + stored, storedErr := s.content.GetRevision(repository.ID, snapshot.RevisionID, session) + if storedErr != nil { + return storedErr + } + if stored.StorageNodeID == snapshot.TargetStorageNodeID { + return nil + } + return errors.New("repository migrated but active revision location is inconsistent") + } + if repository.PrimaryStorageNodeID != snapshot.SourceStorageNodeID || + repository.Status != request.RepositoryStatusMigrationRequired { + return errors.New("repository storage assignment changed during migration") + } + relative := path.Join("public", repository.PublishKey, "revisions", snapshot.RevisionID) + if err = s.content.RelocateRevision(repository.ID, snapshot.RevisionID, snapshot.SourceStorageNodeID, + snapshot.TargetStorageNodeID, relative, session); err != nil { + return err + } + repository.PrimaryStorageNodeID = snapshot.TargetStorageNodeID + repository.PublishBaseURLSnapshot = snapshot.TargetPublishBaseURL + if err = s.repositoryRecords.Update(repository, session); err != nil { + return err + } + return s.repositoryRecords.ActivateRevision(repository.ID, snapshot.RevisionID, + request.RepositoryStatusReady, session) +} + +func migrationPublicKeyURL(baseURL, publishKey, fingerprint string) (string, error) { + value, err := url.Parse(strings.TrimSpace(baseURL)) + if err != nil || (value.Scheme != "http" && value.Scheme != "https") || value.Host == "" || + value.User != nil || value.RawQuery != "" || value.Fragment != "" { + return "", errors.New("publish base URL must be an absolute credential-free HTTP(S) URL") + } + if strings.TrimSpace(publishKey) == "" || (len(fingerprint) != 40 && len(fingerprint) != 64) || + strings.ToUpper(fingerprint) != fingerprint { + return "", errors.New("publish key or GPG fingerprint is invalid") + } + if _, err = hex.DecodeString(fingerprint); err != nil { + return "", errors.New("GPG fingerprint must be hexadecimal") + } + value.Path = strings.TrimRight(value.Path, "/") + "/" + url.PathEscape(publishKey) + + "/keys/" + url.PathEscape(fingerprint) + "/public.key" + value.RawPath = "" + return value.String(), nil +} + +func immutableMigrationURL(baseURL, publishKey, revisionID string) (string, error) { + value, err := url.Parse(strings.TrimSpace(baseURL)) + if err != nil || (value.Scheme != "http" && value.Scheme != "https") || value.Host == "" || + value.User != nil || value.RawQuery != "" || value.Fragment != "" { + return "", errors.New("publish base URL must be an absolute credential-free HTTP(S) URL") + } + if strings.TrimSpace(publishKey) == "" || uuid.Validate(revisionID) != nil { + return "", errors.New("publish key or revision identity is invalid") + } + value.Path = strings.TrimRight(value.Path, "/") + "/" + url.PathEscape(publishKey) + + "/revisions/" + url.PathEscape(revisionID) + value.RawPath = "" + return value.String(), nil +} + +func validMigrationDigest(value string) bool { + if len(value) != 64 || strings.ToLower(value) != value { + return false + } + for _, character := range value { + if (character < '0' || character > '9') && (character < 'a' || character > 'f') { + return false + } + } + return true +} diff --git a/manager/backend/services/footstone/domain/repositoryContentService/migration_integration_test.go b/manager/backend/services/footstone/domain/repositoryContentService/migration_integration_test.go new file mode 100644 index 0000000000000000000000000000000000000000..f37280f1965573217e43a1acb2b6fdd6c63883db --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryContentService/migration_integration_test.go @@ -0,0 +1,235 @@ +//go:build integration + +package repositoryContentService + +import ( + "encoding/json" + "errors" + "os" + "strings" + "testing" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + baseconfig "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + ftpb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/footstone" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorycontentrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositoryrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorystoragenoderepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryNodeService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryTaskService" + appconfig "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" +) + +const ( + migrationProjectID int64 = 920006 + migrationRepoID = "77777777-7777-4777-8777-777777777773" + migrationRevisionID = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaac" + migrationPackageID = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbc" + migrationUploadID = "cccccccc-cccc-4ccc-8ccc-cccccccccccc" + migrationTargetNodeID = "88888888-8888-4888-8888-888888888883" +) + +func TestMigrateRepositoryFreezesRevisionAndRoutesTargetWithMySQL(t *testing.T) { + dsn := os.Getenv("OCM_REPOSITORY_TEST_DSN") + if dsn == "" { + t.Skip("OCM_REPOSITORY_TEST_DSN is not set") + } + db, err := database.NewDBClient(baseconfig.AccessPoint{Source: dsn}) + if err != nil { + t.Fatal(err) + } + defer db.Close() + if err = db.DB().AutoMigrate(&po.Project{}); err != nil { + t.Fatal(err) + } + repositories := repositoryService.New(db) + tasks := repositoryTaskService.New(db) + nodes := repositoryNodeService.New(db) + for _, migrate := range []func() error{repositories.SyncTables, tasks.SyncTables, nodes.SyncTables} { + if err = migrate(); err != nil { + t.Fatal(err) + } + } + store, err := storage.New(appconfig.RepositoryConfig{Root: t.TempDir(), MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + service := New(db, repositories, tasks, store) + if err = service.SyncTables(); err != nil { + t.Fatal(err) + } + service.ConfigureRemoteUploads(nodes, nil) + cleanupMigrationRows(db) + defer cleanupMigrationRows(db) + + project := &po.Project{Base: po.Base{ID: migrationProjectID}, Name: "repository-migration", + EnName: "repository-migration", Creator: "repository-admin"} + if err = db.DB().Create(project).Error; err != nil { + t.Fatal(err) + } + now := time.Now().UTC() + target := &po.RepositoryStorageNode{ID: migrationTargetNodeID, ProjectID: migrationProjectID, + Name: "repository-migration-target", ControlBaseURL: "https://repository-target.test:19443", + PublishBaseURL: "http://repository-target.test:18090/repositories", + TLSServerName: "repository-target.test", Status: request.RepositoryStorageNodeStatusOnline, + Enabled: true, FilesystemType: "xfs", CapacityBytes: 1 << 30, FreeBytes: 1 << 29, + FreeInodes: 100000, ToolVersionsJSON: []byte(`{}`), LabelsJSON: []byte(`{}`), + LastHeartbeatAt: &now, CertificateFingerprint: strings.Repeat("c", 64), CreatedBy: "repository-admin"} + if err = repositorystoragenoderepo.New(db).CreateNode(target, nil); err != nil { + t.Fatal(err) + } + repository := &po.SoftwareRepository{ID: migrationRepoID, ProjectID: migrationProjectID, + SystemProduct: "OpenCloudOS", ProductVersion: "9.6", Name: "legacy-migration-private", + Architecture: request.RepositoryArchitectureX8664, Type: request.RepositoryTypePrivate, + StorageLocation: "public/legacy-migration-private", PublishKey: "legacy-migration-private", + AllowUpload: true, IncludePackagesJSON: "[]", ExcludePackagesJSON: "[]", + GPGPolicy: request.RepositoryGPGDisabled, Enabled: true, + Status: request.RepositoryStatusMigrationRequired, ActiveRevisionID: migrationRevisionID, + PublishBaseURLSnapshot: "http://legacy-manager.test:18090/repositories", + Creator: "repository-admin"} + if err = repositoryrepo.New(db).Create(repository, nil); err != nil { + t.Fatal(err) + } + revision := &po.RepositoryRevision{ID: migrationRevisionID, RepositoryID: repository.ID, + TaskID: "dddddddd-dddd-4ddd-8ddd-dddddddddddd", Kind: request.RepositoryRevisionKindUpload, + Status: request.RepositoryRevisionStatusPublished, PackageCount: 1, TotalBytes: 6201, + ManifestSHA256: strings.Repeat("a", 64), RepomdSHA256: strings.Repeat("b", 64), + SnapshotManifestSHA256: strings.Repeat("c", 64), + RelativePath: "public/legacy-migration-private/revisions/" + migrationRevisionID, + CreatedBy: "repository-admin", PublishedAt: &now} + packageValue := po.RepositoryPackage{ID: migrationPackageID, RepositoryID: repository.ID, + RevisionID: revision.ID, Name: "ocm-private-app", Epoch: "0", Version: "1.0.0", Release: "1.oc9", + Architecture: request.RepositoryArchitectureNoarch, + NEVRA: "ocm-private-app-0:1.0.0-1.oc9.noarch", FileName: "ocm-private-app.rpm", + RelativePath: revision.RelativePath + "/ocm-private-app.rpm", SizeBytes: 6201, + SHA256: strings.Repeat("d", 64), Source: request.RepositoryPackageSourceUpload, + SourceUploadID: migrationUploadID} + if err = repositorycontentrepo.New(db).CreateRevision(revision, []po.RepositoryPackage{packageValue}, nil); err != nil { + t.Fatal(err) + } + ctx := entity.NewDefaultContext() + ctx.User = &ftpb.UserInfo{LoginName: "repository-admin", + Roles: []*ftpb.ModuleRole{{RoleId: request.DefaultGlobalRoleName}}} + + task, err := service.MigrateRepository(ctx, repository.ID, target.ID) + if err != nil { + t.Fatal(err) + } + var snapshot request.RepositoryMigrationSnapshot + if err = json.Unmarshal([]byte(task.PayloadJSON), &snapshot); err != nil { + t.Fatal(err) + } + if task.Type != request.RepositoryTaskTypeMigrateRepository || task.StorageNodeID != target.ID || + task.RevisionID != revision.ID || !task.RequiresMutationLock || + snapshot.SourceStorageNodeID != "" || snapshot.TargetStorageNodeID != target.ID || + snapshot.TargetPublishBaseURL != target.PublishBaseURL || + snapshot.SourceRevisionURL != "http://legacy-manager.test:18090/repositories/"+ + repository.PublishKey+"/revisions/"+revision.ID || + snapshot.SnapshotManifestSHA256 != revision.SnapshotManifestSHA256 || + snapshot.RepomdSHA256 != revision.RepomdSHA256 || snapshot.PackageCount != 1 || + snapshot.TotalBytes != revision.TotalBytes { + t.Fatalf("migration task/snapshot = %#v/%#v", task, snapshot) + } + storedRepository, err := repositoryrepo.New(db).GetByID(repository.ID, nil) + if err != nil || storedRepository.Status != request.RepositoryStatusMigrationRequired || + storedRepository.PrimaryStorageNodeID != "" || storedRepository.ActiveRevisionID != revision.ID { + t.Fatalf("repository migration state = %#v/%v", storedRepository, err) + } + if _, err = tasks.Enqueue(ctx, request.EnqueueRepositoryTaskRequest{ + RepositoryID: repository.ID, Type: request.RepositoryTaskTypeSync, + }); !errors.Is(err, repositoryTaskService.ErrRepositoryMigrationRequired) { + t.Fatalf("migration-required repository accepted normal task: %v", err) + } + if _, err = service.MigrateRepository(ctx, repository.ID, target.ID); !errors.Is(err, ErrRepositoryMigrationActiveTask) { + t.Fatalf("second migration request error = %v", err) + } + claimed, err := tasks.ClaimNextForNode(target.ID, "migration-worker", 30*time.Second) + if err != nil || claimed == nil || claimed.ID != task.ID || claimed.FencingToken == 0 { + t.Fatalf("ClaimNextForNode() task/error = %#v/%v", claimed, err) + } + resultRevision := &po.RepositoryRevision{ID: revision.ID, RepositoryID: repository.ID, + StorageNodeID: target.ID, TaskID: claimed.ID, Kind: revision.Kind, + Status: request.RepositoryRevisionStatusPublished, PackageCount: revision.PackageCount, + TotalBytes: revision.TotalBytes, ManifestSHA256: revision.ManifestSHA256, + RepomdSHA256: revision.RepomdSHA256, SnapshotManifestSHA256: revision.SnapshotManifestSHA256, + RelativePath: revision.RelativePath, PublishedAt: &now} + resultPackages := []po.RepositoryPackage{packageValue} + resultPackages[0].ID = "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee" + prepare := request.PrepareRepositoryTaskCommitRequest{TaskID: claimed.ID, StorageNodeID: target.ID, + WorkerID: claimed.WorkerID, FencingToken: claimed.FencingToken, RevisionID: revision.ID, + ManifestSHA256: revision.SnapshotManifestSHA256, + ResultObjectID: "task-logs/" + claimed.ID + "/result.jsonl.zst", + ResultObjectSHA256: strings.Repeat("e", 64), PackageCount: 1, TotalBytes: revision.TotalBytes, + CompletionStatus: request.RepositoryTaskStatusSuccess} + tamperedPackages := append([]po.RepositoryPackage(nil), resultPackages...) + tamperedPackages[0].SHA256 = strings.Repeat("f", 64) + if _, err = tasks.PrepareCommitWithMutation(prepare, func(tx entity.Session, _ bool) error { + return service.ValidateRemoteMigration(claimed, snapshot, resultRevision, tamperedPackages, tx) + }); err == nil || !strings.Contains(err.Error(), "target package inventory differs") { + t.Fatalf("tampered migration result error = %v", err) + } + prepared, err := tasks.PrepareCommitWithMutation(prepare, func(tx entity.Session, _ bool) error { + return service.ValidateRemoteMigration(claimed, snapshot, resultRevision, resultPackages, tx) + }) + if err != nil || prepared.CommitState != request.RepositoryTaskCommitStatePrepared || + prepared.CommitToken == "" { + t.Fatalf("PrepareCommitWithMutation() task/error = %#v/%v", prepared, err) + } + confirm := request.ConfirmRepositoryTaskCommitRequest{TaskID: claimed.ID, StorageNodeID: target.ID, + WorkerID: claimed.WorkerID, FencingToken: claimed.FencingToken, CommitToken: prepared.CommitToken} + confirmed, err := tasks.ConfirmAndCompleteCommitWithMutation(confirm, + func(tx entity.Session, value *po.RepositoryTask, _ bool) error { + return service.CompleteRemoteMigration(value, snapshot, tx) + }) + if err != nil || confirmed.CommitState != request.RepositoryTaskCommitStateConfirmed || + confirmed.Status != request.RepositoryTaskStatusSuccess { + t.Fatalf("ConfirmAndCompleteCommitWithMutation() task/error = %#v/%v", confirmed, err) + } + if _, err = tasks.ConfirmAndCompleteCommitWithMutation(confirm, + func(tx entity.Session, value *po.RepositoryTask, _ bool) error { + return service.CompleteRemoteMigration(value, snapshot, tx) + }); err != nil { + t.Fatalf("idempotent migration confirmation error = %v", err) + } + storedRepository, err = repositoryrepo.New(db).GetByID(repository.ID, nil) + if err != nil || storedRepository.PrimaryStorageNodeID != target.ID || + storedRepository.PublishBaseURLSnapshot != target.PublishBaseURL || + storedRepository.ActiveRevisionID != revision.ID || storedRepository.Status != request.RepositoryStatusReady { + t.Fatalf("migrated repository state = %#v/%v", storedRepository, err) + } + storedRevision, err := repositorycontentrepo.New(db).GetRevision(repository.ID, revision.ID, nil) + if err != nil || storedRevision.StorageNodeID != target.ID || storedRevision.ID != revision.ID { + t.Fatalf("migrated revision state = %#v/%v", storedRevision, err) + } + var revisionCount, packageCount int64 + if err = db.DB().Model(&po.RepositoryRevision{}).Where("repository_id = ?", repository.ID). + Count(&revisionCount).Error; err != nil { + t.Fatal(err) + } + if err = db.DB().Model(&po.RepositoryPackage{}).Where("repository_id = ?", repository.ID). + Count(&packageCount).Error; err != nil { + t.Fatal(err) + } + if revisionCount != 1 || packageCount != 1 { + t.Fatalf("migration duplicated inventory: revisions=%d packages=%d", revisionCount, packageCount) + } + t.Logf("migration_request=PASS repository=%s revision=%s target=%s manifest=%s", + repository.ID, revision.ID, target.ID, snapshot.SnapshotManifestSHA256) +} + +func cleanupMigrationRows(db *database.DBClient) { + db.DB().Where("repository_id = ?", migrationRepoID).Delete(&po.RepositoryPackage{}) + db.DB().Where("repository_id = ?", migrationRepoID).Delete(&po.RepositoryRevision{}) + db.DB().Where("repository_id = ?", migrationRepoID).Delete(&po.RepositoryTask{}) + db.DB().Where("repository_id = ?", migrationRepoID).Delete(&po.RepositoryMutationLock{}) + db.DB().Where("id = ?", migrationRepoID).Delete(&po.SoftwareRepository{}) + db.DB().Where("storage_node_id = ?", migrationTargetNodeID).Delete(&po.RepositoryPreflightRecord{}) + db.DB().Where("id = ?", migrationTargetNodeID).Delete(&po.RepositoryStorageNode{}) + db.DB().Where("id = ?", migrationProjectID).Delete(&po.Project{}) +} diff --git a/manager/backend/services/footstone/domain/repositoryContentService/package_delete.go b/manager/backend/services/footstone/domain/repositoryContentService/package_delete.go new file mode 100644 index 0000000000000000000000000000000000000000..0b365a80cbd7cea7367576d9151534d9934a9398 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryContentService/package_delete.go @@ -0,0 +1,342 @@ +package repositoryContentService + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "os" + "os/exec" + "sort" + "strings" + "time" + + "github.com/google/uuid" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +const ( + packageDeletionPreviewTTL = 10 * time.Minute + maxDeletionPreviewOutput = 32 << 10 + maxDeletionDependencyEdges = 10000 +) + +type PackageDeletionPreviewResult struct { + Preview *po.RepositoryOperationPreview + Payload request.RepositoryPackageDeletionPreviewPayload +} + +type dnfDependencyPreviewer struct{} + +func newDNFDependencyPreviewer() dependencyPreviewer { return dnfDependencyPreviewer{} } + +func (dnfDependencyPreviewer) Preview(ctx context.Context, baseURL, releaseVersion string, + packageNames []string, +) ([]request.RepositoryDependencyImpact, []string, string, error) { + root, err := os.MkdirTemp("", "ocmanager-dependency-preview-") + if err != nil { + return nil, nil, "", err + } + defer os.RemoveAll(root) + releaseVersion = strings.Split(strings.TrimSpace(releaseVersion), ".")[0] + if releaseVersion == "" { + releaseVersion = "9" + } + common := []string{"-q", "--installroot=" + root, "--releasever=" + releaseVersion, + "--setopt=reposdir=" + root, "--setopt=cachedir=" + root + "/cache", + "--setopt=persistdir=" + root + "/persist", "--disablerepo=*", + "--repofrompath=ocmanager-preview," + baseURL, "--enablerepo=ocmanager-preview", + "repoquery", "--available", "--qf", "%{name}\t%{name}-%{epoch}:%{version}-%{release}.%{arch}"} + commandRecord := make([]string, 0, len(common)+len(packageNames)*2+1) + var raw strings.Builder + impact := make(map[string]request.RepositoryDependencyImpact) + type frontierItem struct { + Capability string + RequiredBy string + Depth int + } + frontier := make([]frontierItem, 0, len(packageNames)) + for _, packageName := range packageNames { + frontier = append(frontier, frontierItem{Capability: packageName, RequiredBy: packageName, Depth: 1}) + } + queried := make(map[string]struct{}, len(packageNames)) + for len(frontier) > 0 { + current := frontier[0] + frontier = frontier[1:] + queryKey := strings.ToLower(strings.TrimSpace(current.Capability)) + if queryKey == "" { + continue + } + if _, exists := queried[queryKey]; exists { + continue + } + queried[queryKey] = struct{}{} + args := append(append([]string{}, common...), "--whatrequires", current.Capability) + commandRecord = append(commandRecord, append([]string{"dnf"}, args...)...) + command := exec.CommandContext(ctx, "dnf", args...) + command.Env = append(os.Environ(), "LC_ALL=C", "LANG=C") + var stdout, stderr bytes.Buffer + command.Stdout, command.Stderr = &stdout, &stderr + commandErr := command.Run() + output := append(append([]byte{}, stdout.Bytes()...), stderr.Bytes()...) + if raw.Len() < maxDeletionPreviewOutput { + remaining := maxDeletionPreviewOutput - raw.Len() + if len(output) > remaining { + output = output[:remaining] + } + raw.Write(output) + } + if commandErr != nil { + return nil, commandRecord, raw.String(), fmt.Errorf("dependency preview for %s: %w", + current.Capability, commandErr) + } + for _, line := range strings.Split(stdout.String(), "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + parts := strings.SplitN(line, "\t", 2) + if len(parts) != 2 || strings.TrimSpace(parts[0]) == "" || strings.TrimSpace(parts[1]) == "" { + return nil, commandRecord, raw.String(), fmt.Errorf( + "dependency preview returned malformed package row %q", line) + } + dependentName, dependentNEVRA := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]) + edgeKey := dependentNEVRA + "\x00" + current.RequiredBy + if _, exists := impact[edgeKey]; exists { + continue + } + impact[edgeKey] = request.RepositoryDependencyImpact{ + PackageNEVRA: dependentNEVRA, + Requires: current.RequiredBy, + Depth: current.Depth, + Direct: current.Depth == 1, + Reason: fmt.Sprintf("%s requires %s", dependentNEVRA, current.RequiredBy), + } + if len(impact) > maxDeletionDependencyEdges { + return nil, commandRecord, raw.String(), fmt.Errorf( + "dependency preview exceeds %d reverse dependency edges", maxDeletionDependencyEdges) + } + frontier = append(frontier, frontierItem{Capability: dependentName, + RequiredBy: dependentNEVRA, Depth: current.Depth + 1}) + } + } + result := make([]request.RepositoryDependencyImpact, 0, len(impact)) + for _, value := range impact { + result = append(result, value) + } + sort.Slice(result, func(left, right int) bool { + if result[left].Depth != result[right].Depth { + return result[left].Depth < result[right].Depth + } + if result[left].PackageNEVRA == result[right].PackageNEVRA { + return result[left].Requires < result[right].Requires + } + return result[left].PackageNEVRA < result[right].PackageNEVRA + }) + return result, commandRecord, raw.String(), nil +} + +func (s *service) PreviewPackageDeletion(ctx entity.Context, input request.PreviewRepositoryPackageDeletionRequest) ( + *PackageDeletionPreviewResult, error, +) { + repository, err := s.repositories.Describe(ctx, strings.TrimSpace(input.RepositoryID)) + if err != nil { + return nil, err + } + if repository.ActiveRevisionID == "" { + return nil, errors.New("repository has no active revision") + } + if ctx.User == nil || strings.TrimSpace(ctx.User.LoginName) == "" { + return nil, errors.New("authenticated user is required") + } + nevras, err := normalizeDeletionNEVRAs(input.NEVRAs) + if err != nil { + return nil, err + } + packages, err := s.content.PackagesByRevision(repository.ActiveRevisionID, nil) + if err != nil { + return nil, err + } + selected := make(map[string]struct{}, len(nevras)) + for _, nevra := range nevras { + selected[nevra] = struct{}{} + } + packageNames := make([]string, 0, len(nevras)) + found := make(map[string]struct{}, len(nevras)) + for _, item := range packages { + if _, ok := selected[item.NEVRA]; ok { + found[item.NEVRA] = struct{}{} + packageNames = append(packageNames, item.Name) + } + } + if len(found) != len(selected) { + return nil, errors.New("one or more packages do not exist in the active revision") + } + packageNames = uniqueSorted(packageNames) + config, err := s.GenerateRepositoryConfig(ctx, repository.ID, true) + if err != nil { + return nil, err + } + if config.ActiveRevisionID != repository.ActiveRevisionID || !config.ImmutableRevision { + return nil, ErrDeletionRevisionChanged + } + impact, command, rawOutput, err := s.dependencyPreview.Preview(ctx.Context, config.RevisionURL, + repository.ProductVersion, packageNames) + if err != nil { + return nil, err + } + filteredImpact := make([]request.RepositoryDependencyImpact, 0, len(impact)) + for _, dependency := range impact { + if _, deleting := selected[dependency.PackageNEVRA]; !deleting { + filteredImpact = append(filteredImpact, dependency) + } + } + previewID := uuid.NewString() + payload := request.RepositoryPackageDeletionPreviewPayload{ + PreviewID: previewID, RepositoryID: repository.ID, RevisionID: repository.ActiveRevisionID, + RepositorySet: []request.RepositoryPreviewSource{{RepositoryID: repository.ID, + RevisionID: repository.ActiveRevisionID, RepoID: config.RepoID, BaseURL: config.RevisionURL}}, + NEVRAs: nevras, DependencyImpact: filteredImpact, Command: command, RawOutput: rawOutput, + } + payloadJSON, err := json.Marshal(payload) + if err != nil { + return nil, err + } + digest := sha256.Sum256(payloadJSON) + now := s.now() + preview := &po.RepositoryOperationPreview{ID: previewID, ProjectID: repository.ProjectID, + OperationType: request.RepositoryOperationPackageDelete, ResourceID: repository.ID, + ResourceVersion: repository.ActiveRevisionID, PreviewDigest: hex.EncodeToString(digest[:]), + PayloadJSON: payloadJSON, Status: request.RepositoryOperationPreviewReady, + ExpiresAt: now.Add(packageDeletionPreviewTTL), CreatedBy: ctx.User.LoginName} + if err = s.previews.CreatePreview(preview, nil); err != nil { + return nil, err + } + return &PackageDeletionPreviewResult{Preview: preview, Payload: payload}, nil +} + +func (s *service) ConfirmPackageDeletion(ctx entity.Context, + input request.ConfirmRepositoryPackageDeletionRequest, +) (task *po.RepositoryTask, err error) { + input.PreviewID = strings.TrimSpace(input.PreviewID) + input.PreviewDigest = strings.ToLower(strings.TrimSpace(input.PreviewDigest)) + if uuid.Validate(input.PreviewID) != nil || len(input.PreviewDigest) != sha256.Size*2 { + return nil, ErrDeletionPreviewChanged + } + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + preview, err := s.previews.GetPreview(input.PreviewID, true, tx) + if err != nil { + return nil, err + } + if preview.OperationType != request.RepositoryOperationPackageDelete || preview.PreviewDigest != input.PreviewDigest { + return nil, ErrDeletionPreviewChanged + } + if ctx.User == nil || preview.CreatedBy != ctx.User.LoginName { + return nil, ErrDeletionPreviewForbidden + } + if preview.Status != request.RepositoryOperationPreviewReady && + preview.Status != request.RepositoryOperationPreviewConfirmed { + return nil, ErrDeletionPreviewChanged + } + if !s.now().Before(preview.ExpiresAt) { + return nil, ErrDeletionPreviewExpired + } + var payload request.RepositoryPackageDeletionPreviewPayload + if err = json.Unmarshal(preview.PayloadJSON, &payload); err != nil || payload.RepositoryID != preview.ResourceID || + payload.RevisionID != preview.ResourceVersion || payload.PreviewID != preview.ID { + return nil, ErrDeletionPreviewChanged + } + repository, err := s.repositoryRecords.GetByID(payload.RepositoryID, tx) + if err != nil || repository.ProjectID != preview.ProjectID || repository.ActiveRevisionID != payload.RevisionID { + return nil, ErrDeletionRevisionChanged + } + taskPayload, err := json.Marshal(map[string]interface{}{"nevras": payload.NEVRAs, + "preview_id": preview.ID, "preview_digest": preview.PreviewDigest, + "revision_id": payload.RevisionID, "repository_set": payload.RepositorySet}) + if err != nil { + return nil, err + } + task, err = s.tasks.EnqueueInSession(ctx, request.EnqueueRepositoryTaskRequest{ + RepositoryID: repository.ID, Type: request.RepositoryTaskTypeDeletePackage, + PayloadJSON: string(taskPayload), MaxAttempts: 3, + IdempotencyKey: "package-delete:" + preview.ID, + }, tx) + if err != nil { + return nil, err + } + if preview.Status != request.RepositoryOperationPreviewConfirmed { + now := s.now() + preview.Status, preview.ConfirmedAt = request.RepositoryOperationPreviewConfirmed, &now + if err = s.previews.UpdatePreview(preview, tx); err != nil { + return nil, err + } + } + return task, nil +} + +// CompleteRemotePackageDeletion atomically switches the control-plane active +// revision and records every selected package result after the repository node +// has already published the immutable revision. +func (s *service) CompleteRemotePackageDeletion(task *po.RepositoryTask, session entity.Session) error { + if task == nil || task.Type != request.RepositoryTaskTypeDeletePackage || session == nil || + uuid.Validate(task.PreparedRevisionID) != nil { + return errors.New("prepared delete package task and transaction are required") + } + var payload struct { + NEVRAs []string `json:"nevras"` + } + if err := json.Unmarshal([]byte(task.PayloadJSON), &payload); err != nil { + return ErrDeletionPreviewChanged + } + nevras, err := normalizeDeletionNEVRAs(payload.NEVRAs) + if err != nil { + return err + } + if err = s.repositoryRecords.ActivateRevision(task.RepositoryID, task.PreparedRevisionID, + request.RepositoryStatusReady, session); err != nil { + return err + } + return s.tasks.RecordPackageDeleteResults(task, nevras, session) +} + +func normalizeDeletionNEVRAs(values []string) ([]string, error) { + if len(values) == 0 || len(values) > 100 { + return nil, errors.New("nevras must contain between 1 and 100 packages") + } + seen := make(map[string]struct{}, len(values)) + result := make([]string, 0, len(values)) + for _, value := range values { + value = strings.TrimSpace(value) + if value == "" || len(value) > 512 { + return nil, errors.New("package NEVRA is invalid") + } + if _, ok := seen[value]; ok { + continue + } + seen[value] = struct{}{} + result = append(result, value) + } + sort.Strings(result) + return result, nil +} + +func uniqueSorted(values []string) []string { + seen := make(map[string]struct{}, len(values)) + result := make([]string, 0, len(values)) + for _, value := range values { + if _, ok := seen[value]; ok { + continue + } + seen[value] = struct{}{} + result = append(result, value) + } + sort.Strings(result) + return result +} diff --git a/manager/backend/services/footstone/domain/repositoryContentService/package_delete_test.go b/manager/backend/services/footstone/domain/repositoryContentService/package_delete_test.go new file mode 100644 index 0000000000000000000000000000000000000000..aa0f9daa0c1eb42222efa3e6cdd4a4197ff10fa2 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryContentService/package_delete_test.go @@ -0,0 +1,97 @@ +package repositoryContentService + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestDNFDependencyPreviewDisablesDefaultRepositories(t *testing.T) { + root := t.TempDir() + bin := filepath.Join(root, "bin") + if err := os.MkdirAll(bin, 0755); err != nil { + t.Fatal(err) + } + capture := filepath.Join(root, "args.txt") + script := "#!/bin/sh\nprintf '%s\\n' \"$@\" >> \"$OCM_DNF_CAPTURE\"\n" + + "case \"$*\" in\n" + + " *'--whatrequires dependency'*) printf 'dependent\\tdependent-0:1.0-1.noarch\\n' ;;\n" + + "esac\n" + if err := os.WriteFile(filepath.Join(bin, "dnf"), []byte(script), 0755); err != nil { + t.Fatal(err) + } + t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH")) + t.Setenv("OCM_DNF_CAPTURE", capture) + impact, _, _, err := (dnfDependencyPreviewer{}).Preview(context.Background(), + "https://repo.example.test/repositories/key/revisions/revision-id/", "9.6", []string{"dependency"}) + if err != nil { + t.Fatal(err) + } + if len(impact) != 1 || impact[0].PackageNEVRA != "dependent-0:1.0-1.noarch" || + !impact[0].Direct || impact[0].Depth != 1 { + t.Fatalf("dependency impact = %#v", impact) + } + arguments, err := os.ReadFile(capture) + if err != nil { + t.Fatal(err) + } + text := string(arguments) + for _, required := range []string{"--disablerepo=*", "--enablerepo=ocmanager-preview", + "--repofrompath=ocmanager-preview,https://repo.example.test/repositories/key/revisions/revision-id/", + "--whatrequires", "dependency"} { + if !strings.Contains(text, required) { + t.Fatalf("dependency preview arguments omit %q: %s", required, text) + } + } +} + +func TestDNFDependencyPreviewBuildsTransitiveReverseDependencyClosure(t *testing.T) { + root := t.TempDir() + bin := filepath.Join(root, "bin") + if err := os.MkdirAll(bin, 0755); err != nil { + t.Fatal(err) + } + script := `#!/bin/sh +case "$*" in + *'--whatrequires dependency'*) printf 'direct-app\tdirect-app-0:1.0-1.noarch\n' ;; + *'--whatrequires direct-app'*) printf 'top-app\ttop-app-0:1.0-1.noarch\n' ;; +esac +` + if err := os.WriteFile(filepath.Join(bin, "dnf"), []byte(script), 0755); err != nil { + t.Fatal(err) + } + t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH")) + impact, command, _, err := (dnfDependencyPreviewer{}).Preview(context.Background(), + "https://repo.example.test/repositories/key/revisions/revision-id/", "9.6", []string{"dependency"}) + if err != nil { + t.Fatal(err) + } + if len(impact) != 2 { + t.Fatalf("dependency impact = %#v", impact) + } + if impact[0].PackageNEVRA != "direct-app-0:1.0-1.noarch" || !impact[0].Direct || impact[0].Depth != 1 || + impact[0].Requires != "dependency" { + t.Fatalf("direct dependency impact = %#v", impact[0]) + } + if impact[1].PackageNEVRA != "top-app-0:1.0-1.noarch" || impact[1].Direct || impact[1].Depth != 2 || + impact[1].Requires != "direct-app-0:1.0-1.noarch" { + t.Fatalf("transitive dependency impact = %#v", impact[1]) + } + joined := strings.Join(command, " ") + if !strings.Contains(joined, "--whatrequires dependency") || + !strings.Contains(joined, "--whatrequires direct-app") { + t.Fatalf("dependency command trace = %s", joined) + } +} + +func TestNormalizeDeletionNEVRAsDeduplicatesAndSorts(t *testing.T) { + values, err := normalizeDeletionNEVRAs([]string{"b-0:1-1.noarch", "a-0:1-1.noarch", "b-0:1-1.noarch"}) + if err != nil { + t.Fatal(err) + } + if strings.Join(values, ",") != "a-0:1-1.noarch,b-0:1-1.noarch" { + t.Fatalf("normalized NEVRAs = %#v", values) + } +} diff --git a/manager/backend/services/footstone/domain/repositoryContentService/remote_upload_retry.go b/manager/backend/services/footstone/domain/repositoryContentService/remote_upload_retry.go new file mode 100644 index 0000000000000000000000000000000000000000..f98f13292e2a1f5c3eee146913e3b47b5924c9f5 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryContentService/remote_upload_retry.go @@ -0,0 +1,98 @@ +package repositoryContentService + +import ( + "encoding/json" + "errors" + "strings" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +var errRemoteUploadRetryInvalid = errors.New("remote upload retry metadata or task identity is invalid") + +// PrepareRemoteUploadRetry only runs inside the task-owned result-import transaction, +// after the worker has verified the frozen object and task ownership has been checked. +func (s *service) PrepareRemoteUploadRetry(task *po.RepositoryTask, snapshot request.RepositoryUploadSnapshot, + session entity.Session, +) error { + if task == nil || session == nil || task.Type != request.RepositoryTaskTypeUpload { + return errRemoteUploadRetryInvalid + } + if s.uploadTasks == nil || task.RetryOfTaskID == "" || task.ID == task.RootTaskID || task.ID == task.RetryOfTaskID || + task.RootTaskID == "" || task.Status != request.RepositoryTaskStatusRunning || + task.WorkerID == "" || task.FencingToken == 0 { + return errRemoteUploadRetryInvalid + } + persisted, err := s.uploadTasks.GetByID(task.ID, session) + if err != nil { + return err + } + if !sameUploadRetryTask(task, persisted) || persisted.LeaseExpiresAt == nil || + !persisted.LeaseExpiresAt.After(s.now()) { + return errRemoteUploadRetryInvalid + } + root, err := s.uploadTasks.GetByID(task.RootTaskID, session) + if err != nil { + return err + } + previous := root + if task.RetryOfTaskID != task.RootTaskID { + previous, err = s.uploadTasks.GetByID(task.RetryOfTaskID, session) + if err != nil { + return err + } + } + if !sameUploadRetryScope(task, root) || !sameUploadRetryScope(task, previous) || + root.ID != task.RootTaskID || root.RootTaskID != root.ID || + previous.ID != task.RetryOfTaskID || previous.RootTaskID != task.RootTaskID || + !failedUploadTask(root.Status) || !failedUploadTask(previous.Status) { + return errRemoteUploadRetryInvalid + } + for _, value := range []*po.RepositoryTask{persisted, root, previous} { + var storedSnapshot request.RepositoryUploadSnapshot + if json.Unmarshal([]byte(value.PayloadJSON), &storedSnapshot) != nil || storedSnapshot != snapshot { + return errRemoteUploadRetryInvalid + } + } + upload, err := s.content.GetUpload(snapshot.UploadID, true, session) + if err != nil { + return err + } + if upload == nil || upload.TaskID != root.ID || upload.RequestedBy != root.RequestedBy || + upload.ProjectID != task.ProjectID || upload.RepositoryID != task.RepositoryID || + upload.StorageNodeID != task.StorageNodeID || upload.FileName != snapshot.FileName || + upload.Kind != snapshot.Kind || upload.RelativePath != snapshot.RelativePath || + upload.ExpectedSize != snapshot.SizeBytes || upload.ActualSHA256 != snapshot.SHA256 { + return errRemoteUploadRetryInvalid + } + switch upload.Status { + case request.RepositoryUploadStatusReady, request.RepositoryUploadStatusCompleted: + return nil + case request.RepositoryUploadStatusFailed: + upload.Status = request.RepositoryUploadStatusReady + upload.ErrorCode, upload.ErrorMessage = "", "" + return s.content.UpdateUpload(upload, session) + default: + return errRemoteUploadRetryInvalid + } +} + +func sameUploadRetryTask(expected, persisted *po.RepositoryTask) bool { + return persisted != nil && expected.ID == persisted.ID && + expected.RootTaskID == persisted.RootTaskID && expected.RetryOfTaskID == persisted.RetryOfTaskID && + expected.Status == persisted.Status && expected.WorkerID == persisted.WorkerID && + expected.FencingToken == persisted.FencingToken && expected.PayloadJSON == persisted.PayloadJSON && + expected.RequestedBy == persisted.RequestedBy && sameUploadRetryScope(expected, persisted) +} + +func sameUploadRetryScope(expected, value *po.RepositoryTask) bool { + return value != nil && value.Type == request.RepositoryTaskTypeUpload && + expected.ProjectID == value.ProjectID && expected.RepositoryID == value.RepositoryID && + expected.StorageNodeID == value.StorageNodeID && strings.TrimSpace(value.RequestedBy) != "" +} + +func failedUploadTask(status string) bool { + return status == request.RepositoryTaskStatusFailed || status == request.RepositoryTaskStatusCanceled +} diff --git a/manager/backend/services/footstone/domain/repositoryContentService/remote_upload_retry_test.go b/manager/backend/services/footstone/domain/repositoryContentService/remote_upload_retry_test.go new file mode 100644 index 0000000000000000000000000000000000000000..129924c22fd2bd6c94e6cbe4826c454588f31d83 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryContentService/remote_upload_retry_test.go @@ -0,0 +1,256 @@ +package repositoryContentService + +import ( + "encoding/json" + "errors" + "strings" + "testing" + "time" + + "gorm.io/gorm" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +func TestUploadTaskMatches(t *testing.T) { + const ( + rootTaskID = "11111111-1111-4111-8111-111111111111" + retryTaskID = "22222222-2222-4222-8222-222222222222" + ) + tests := []struct { + name string + uploadTaskID string + task *po.RepositoryTask + want bool + }{ + {name: "original task", uploadTaskID: rootTaskID, + task: &po.RepositoryTask{ID: rootTaskID, RootTaskID: rootTaskID}, want: true}, + {name: "retry uses root upload", uploadTaskID: rootTaskID, + task: &po.RepositoryTask{ID: retryTaskID, RootTaskID: rootTaskID}, want: true}, + {name: "unrelated upload", uploadTaskID: retryTaskID, + task: &po.RepositoryTask{ID: rootTaskID, RootTaskID: rootTaskID}, want: false}, + {name: "missing task", uploadTaskID: rootTaskID, task: nil, want: false}, + {name: "missing upload task id", uploadTaskID: "", + task: &po.RepositoryTask{ID: rootTaskID, RootTaskID: rootTaskID}, want: false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := uploadTaskMatches(tt.uploadTaskID, tt.task); got != tt.want { + t.Fatalf("uploadTaskMatches() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPrepareRemoteUploadRetryAllowsValidatedRetryAndIsIdempotent(t *testing.T) { + service, task, snapshot, records, _, session := newRemoteUploadRetryFixture(t) + if err := service.PrepareRemoteUploadRetry(task, snapshot, session); err != nil { + t.Fatal(err) + } + if records.value.Status != request.RepositoryUploadStatusReady || records.updates != 1 || + records.value.ErrorCode != "" || records.value.ErrorMessage != "" { + t.Fatalf("retry preparation did not clear the old failure: %#v", records.value) + } + if err := service.ApplyRemoteUploadOutcome(task, snapshot, request.RepositoryTaskStatusSuccess, "", "", session); err != nil { + t.Fatal(err) + } + if records.value.Status != request.RepositoryUploadStatusCompleted || records.updates != 2 { + t.Fatalf("retry outcome did not complete the upload: %#v", records.value) + } + if err := service.PrepareRemoteUploadRetry(task, snapshot, session); err != nil { + t.Fatalf("prepared result replay failed: %v", err) + } + if err := service.ApplyRemoteUploadOutcome(task, snapshot, request.RepositoryTaskStatusSuccess, "", "", session); err != nil { + t.Fatalf("result replay failed: %v", err) + } + if records.updates != 2 { + t.Fatalf("idempotent result import wrote %d updates, want 2", records.updates) + } +} + +func TestApplyRemoteUploadOutcomeStillRejectsFailedUploadWithoutRetryPreparation(t *testing.T) { + service, task, snapshot, records, _, session := newRemoteUploadRetryFixture(t) + err := service.ApplyRemoteUploadOutcome(task, snapshot, request.RepositoryTaskStatusSuccess, "", "", session) + if err == nil || !strings.Contains(err.Error(), "not ready") || records.updates != 0 || + records.value.Status != request.RepositoryUploadStatusFailed { + t.Fatalf("direct failed-upload outcome was accepted: error=%v upload=%#v", err, records.value) + } +} + +func TestPrepareRemoteUploadRetryRejectsIdentityAndStateChanges(t *testing.T) { + for _, fixture := range []struct { + name string + mutate func(*service, *po.RepositoryTask, *request.RepositoryUploadSnapshot, *uploadRetryRecords, *uploadRetryTaskRecords) + }{ + {name: "unpersisted task", mutate: func(_ *service, task *po.RepositoryTask, _ *request.RepositoryUploadSnapshot, _ *uploadRetryRecords, tasks *uploadRetryTaskRecords) { + delete(tasks.values, task.ID) + }}, + {name: "forged root", mutate: func(_ *service, task *po.RepositoryTask, _ *request.RepositoryUploadSnapshot, _ *uploadRetryRecords, _ *uploadRetryTaskRecords) { + task.RootTaskID = "99999999-9999-4999-8999-999999999999" + }}, + {name: "forged previous task", mutate: func(_ *service, task *po.RepositoryTask, _ *request.RepositoryUploadSnapshot, _ *uploadRetryRecords, _ *uploadRetryTaskRecords) { + task.RetryOfTaskID = "99999999-9999-4999-8999-999999999999" + }}, + {name: "another project", mutate: func(_ *service, task *po.RepositoryTask, _ *request.RepositoryUploadSnapshot, _ *uploadRetryRecords, tasks *uploadRetryTaskRecords) { + tasks.values[task.RootTaskID].ProjectID++ + }}, + {name: "another node", mutate: func(_ *service, _ *po.RepositoryTask, _ *request.RepositoryUploadSnapshot, records *uploadRetryRecords, _ *uploadRetryTaskRecords) { + records.value.StorageNodeID = "99999999-9999-4999-8999-999999999999" + }}, + {name: "another owner", mutate: func(_ *service, _ *po.RepositoryTask, _ *request.RepositoryUploadSnapshot, records *uploadRetryRecords, _ *uploadRetryTaskRecords) { + records.value.RequestedBy = "different-uploader" + }}, + {name: "changed snapshot", mutate: func(_ *service, _ *po.RepositoryTask, snapshot *request.RepositoryUploadSnapshot, _ *uploadRetryRecords, _ *uploadRetryTaskRecords) { + snapshot.SHA256 = strings.Repeat("b", 64) + }}, + {name: "changed stored digest", mutate: func(_ *service, _ *po.RepositoryTask, _ *request.RepositoryUploadSnapshot, records *uploadRetryRecords, _ *uploadRetryTaskRecords) { + records.value.ActualSHA256 = strings.Repeat("b", 64) + }}, + {name: "pending retry", mutate: func(_ *service, task *po.RepositoryTask, _ *request.RepositoryUploadSnapshot, _ *uploadRetryRecords, _ *uploadRetryTaskRecords) { + task.Status = request.RepositoryTaskStatusPending + }}, + {name: "missing worker", mutate: func(_ *service, task *po.RepositoryTask, _ *request.RepositoryUploadSnapshot, _ *uploadRetryRecords, _ *uploadRetryTaskRecords) { + task.WorkerID = "" + }}, + {name: "changed fencing token", mutate: func(_ *service, task *po.RepositoryTask, _ *request.RepositoryUploadSnapshot, _ *uploadRetryRecords, _ *uploadRetryTaskRecords) { + task.FencingToken++ + }}, + {name: "expired lease", mutate: func(service *service, task *po.RepositoryTask, _ *request.RepositoryUploadSnapshot, _ *uploadRetryRecords, tasks *uploadRetryTaskRecords) { + past := service.now().Add(-time.Second) + tasks.values[task.ID].LeaseExpiresAt = &past + }}, + {name: "successful root", mutate: func(_ *service, task *po.RepositoryTask, _ *request.RepositoryUploadSnapshot, _ *uploadRetryRecords, tasks *uploadRetryTaskRecords) { + tasks.values[task.RootTaskID].Status = request.RepositoryTaskStatusSuccess + }}, + {name: "aborted upload", mutate: func(_ *service, _ *po.RepositoryTask, _ *request.RepositoryUploadSnapshot, records *uploadRetryRecords, _ *uploadRetryTaskRecords) { + records.value.Status = request.RepositoryUploadStatusAborted + }}, + } { + t.Run(fixture.name, func(t *testing.T) { + service, task, snapshot, records, tasks, session := newRemoteUploadRetryFixture(t) + fixture.mutate(service, task, &snapshot, records, tasks) + before := *records.value + if err := service.PrepareRemoteUploadRetry(task, snapshot, session); err == nil { + t.Fatal("invalid retry was accepted") + } + if records.updates != 0 || *records.value != before { + t.Fatalf("rejected retry mutated the upload: %#v", records.value) + } + }) + } +} + +func TestPrepareRemoteUploadRetryDoesNotReopenOriginalFailedTask(t *testing.T) { + service, retry, snapshot, records, tasks, session := newRemoteUploadRetryFixture(t) + root := tasks.values[retry.RootTaskID] + if err := service.PrepareRemoteUploadRetry(root, snapshot, session); err == nil { + t.Fatal("original failed task was accepted as a retry") + } + if records.value.Status != request.RepositoryUploadStatusFailed || records.updates != 0 { + t.Fatal("non-retry task reopened a failed upload") + } +} + +func TestPrepareRemoteUploadRetryValidatesPreviousRetryChain(t *testing.T) { + for _, valid := range []bool{true, false} { + name := "valid previous retry" + if !valid { + name = "unrelated previous retry" + } + t.Run(name, func(t *testing.T) { + service, task, snapshot, records, tasks, session := newRemoteUploadRetryFixture(t) + previousID := "66666666-6666-4666-8666-666666666666" + previous := *task + previous.ID, previous.Status = previousID, request.RepositoryTaskStatusFailed + task.RetryOfTaskID = previousID + persisted := *task + tasks.values[task.ID] = &persisted + if !valid { + previous.RootTaskID = "77777777-7777-4777-8777-777777777777" + } + tasks.values[previousID] = &previous + err := service.PrepareRemoteUploadRetry(task, snapshot, session) + if valid && (err != nil || records.value.Status != request.RepositoryUploadStatusReady) { + t.Fatalf("valid chained retry failed: %v", err) + } + if !valid && (err == nil || records.updates != 0) { + t.Fatalf("unrelated chained retry was accepted: %v", err) + } + }) + } +} + +type uploadRetryRecords struct { + uploadSessionMemoryRepository + value *po.RepositoryUpload + updates int +} + +func (r *uploadRetryRecords) GetUpload(id string, _ bool, _ entity.Session) (*po.RepositoryUpload, error) { + if r.value == nil || r.value.ID != id { + return nil, errors.New("upload not found") + } + value := *r.value + return &value, nil +} + +func (r *uploadRetryRecords) UpdateUpload(value *po.RepositoryUpload, _ entity.Session) error { + copyValue := *value + r.value, r.updates = ©Value, r.updates+1 + return nil +} + +type uploadRetryTaskRecords struct { + values map[string]*po.RepositoryTask +} + +func (r *uploadRetryTaskRecords) GetByID(id string, _ entity.Session) (*po.RepositoryTask, error) { + value := r.values[id] + if value == nil { + return nil, errors.New("task not found") + } + copyValue := *value + return ©Value, nil +} + +func newRemoteUploadRetryFixture(t *testing.T) (*service, *po.RepositoryTask, request.RepositoryUploadSnapshot, + *uploadRetryRecords, *uploadRetryTaskRecords, entity.Session, +) { + t.Helper() + clock := time.Date(2026, 10, 23, 0, 0, 0, 0, time.UTC) + lease := clock.Add(time.Minute) + const ( + rootID = "11111111-1111-4111-8111-111111111111" + retryID = "22222222-2222-4222-8222-222222222222" + repositoryID = "33333333-3333-4333-8333-333333333333" + nodeID = "44444444-4444-4444-8444-444444444444" + uploadID = "55555555-5555-4555-8555-555555555555" + ) + snapshot := request.RepositoryUploadSnapshot{UploadID: uploadID, FileName: "same-name.rpm", + Kind: request.RepositoryUploadKindRPM, RelativePath: "staging/uploads/" + uploadID + "/content", + SizeBytes: 8, SHA256: strings.Repeat("a", 64)} + payload, err := json.Marshal(snapshot) + if err != nil { + t.Fatal(err) + } + root := &po.RepositoryTask{ID: rootID, RootTaskID: rootID, ProjectID: 42, RepositoryID: repositoryID, + StorageNodeID: nodeID, Type: request.RepositoryTaskTypeUpload, Status: request.RepositoryTaskStatusFailed, + RequestedBy: "original-uploader", PayloadJSON: string(payload)} + retry := &po.RepositoryTask{ID: retryID, RootTaskID: rootID, RetryOfTaskID: rootID, ProjectID: 42, + RepositoryID: repositoryID, StorageNodeID: nodeID, Type: request.RepositoryTaskTypeUpload, + Status: request.RepositoryTaskStatusRunning, RequestedBy: "authorized-retry-admin", PayloadJSON: string(payload), + WorkerID: "review-worker", FencingToken: 5, LeaseExpiresAt: &lease} + persistedRetry := *retry + tasks := &uploadRetryTaskRecords{values: map[string]*po.RepositoryTask{rootID: root, retryID: &persistedRetry}} + records := &uploadRetryRecords{value: &po.RepositoryUpload{ + ID: uploadID, TaskID: rootID, ProjectID: 42, RepositoryID: repositoryID, StorageNodeID: nodeID, + RequestedBy: root.RequestedBy, Status: request.RepositoryUploadStatusFailed, + FileName: snapshot.FileName, Kind: snapshot.Kind, RelativePath: snapshot.RelativePath, + ExpectedSize: snapshot.SizeBytes, ActualSHA256: snapshot.SHA256, + ErrorCode: "GPG_KEY_SYNC_REQUIRED", ErrorMessage: "authorized public key was not ready", + }} + value := &service{content: records, uploadTasks: tasks, now: func() time.Time { return clock }} + return value, retry, snapshot, records, tasks, &gorm.DB{} +} diff --git a/manager/backend/services/footstone/domain/repositoryContentService/repo_config.go b/manager/backend/services/footstone/domain/repositoryContentService/repo_config.go new file mode 100644 index 0000000000000000000000000000000000000000..50743303c61d803f41ed37a0f8c1d6162bbcb5ff --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryContentService/repo_config.go @@ -0,0 +1,166 @@ +package repositoryContentService + +import ( + "errors" + "fmt" + "net/url" + "regexp" + "strings" + "unicode" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +var ( + ErrRepositoryNotPublished = errors.New("repository has no published active revision") + ErrRepositoryGPGKeyUnavailable = errors.New("repository GPG public key is not available") + repositoryConfigIDPattern = regexp.MustCompile(`^[A-Za-z0-9._-]+$`) +) + +func (s *service) GenerateRepositoryConfig(ctx entity.Context, repositoryID string, immutableRevision bool) ( + *request.GeneratedRepositoryConfig, error, +) { + repository, err := s.repositories.Describe(ctx, strings.TrimSpace(repositoryID)) + if err != nil { + return nil, err + } + if repository.Status != request.RepositoryStatusReady || strings.TrimSpace(repository.ActiveRevisionID) == "" { + return nil, ErrRepositoryNotPublished + } + revision, err := s.content.GetRevision(repository.ID, repository.ActiveRevisionID, nil) + if err != nil { + return nil, err + } + if revision.Status != request.RepositoryRevisionStatusPublished || revision.PublishedAt == nil || + revision.ID != repository.ActiveRevisionID { + return nil, ErrRepositoryNotPublished + } + var baseURL, revisionURL string + if s.remoteUpload != nil { + if revision.StorageNodeID == "" || revision.StorageNodeID != repository.PrimaryStorageNodeID { + return nil, errors.New("active revision is not located on the repository primary storage node") + } + baseURL, revisionURL, err = remotePublicationURLs(repository.PublishBaseURLSnapshot, + repository.PublishKey, revision.ID) + } else { + if err = s.store.VerifyPublishedCurrent(repository.PublishKey, revision.ID); err != nil { + return nil, err + } + baseURL, err = s.store.PublicCurrentURL(repository.PublishKey) + if err == nil { + revisionURL, err = s.store.PublicRevisionURL(repository.PublishKey, revision.ID) + } + } + if err != nil { + return nil, err + } + if immutableRevision { + baseURL = revisionURL + } + gpgKeyURLs := []string{} + if request.RepositoryGPGPolicyEnforced(repository.GPGPolicy) { + keys, listErr := s.ListGPGKeys(ctx, repository.ID) + if listErr != nil { + return nil, listErr + } + if len(keys) == 0 { + return nil, ErrRepositoryGPGKeyUnavailable + } + for _, key := range keys { + gpgKeyURLs = append(gpgKeyURLs, key.PublicKeyURL) + } + } + if revision.MetadataSigned { + if revision.RepomdSignaturePath != "repodata/repomd.xml.asc" || + strings.TrimSpace(revision.MetadataPublicKeyURL) == "" { + return nil, errors.New("active revision metadata signature is incomplete") + } + gpgKeyURLs = append(gpgKeyURLs, revision.MetadataPublicKeyURL) + } + return buildRepositoryConfig(repository, revision, baseURL, revisionURL, immutableRevision, gpgKeyURLs) +} + +func remotePublicationURLs(baseURL, publishKey, revisionID string) (string, string, error) { + current, err := url.Parse(strings.TrimSpace(baseURL)) + if err != nil || (current.Scheme != "http" && current.Scheme != "https") || current.Host == "" || + current.User != nil || current.RawQuery != "" || current.Fragment != "" { + return "", "", errors.New("publish base URL must be an absolute credential-free HTTP(S) URL") + } + if !repositoryConfigIDPattern.MatchString(publishKey) { + return "", "", errors.New("repository publish key is invalid") + } + revisionURL, err := immutableMigrationURL(current.String(), publishKey, revisionID) + if err != nil { + return "", "", err + } + current.Path = strings.TrimRight(current.Path, "/") + "/" + url.PathEscape(publishKey) + "/current" + current.RawPath = "" + return current.String(), revisionURL, nil +} + +func buildRepositoryConfig(repository *po.SoftwareRepository, revision *po.RepositoryRevision, + baseURL, revisionURL string, immutableRevision bool, gpgKeyURLs []string, +) (*request.GeneratedRepositoryConfig, error) { + if repository == nil || revision == nil || revision.ID == "" || revision.ID != repository.ActiveRevisionID || + revision.Status != request.RepositoryRevisionStatusPublished || revision.PublishedAt == nil { + return nil, ErrRepositoryNotPublished + } + if repository.PublishKey == "" || len(repository.PublishKey) > 64 || + !repositoryConfigIDPattern.MatchString(repository.PublishKey) { + return nil, fmt.Errorf("repository publish key is invalid for a DNF repository ID") + } + if strings.IndexFunc(repository.SystemProduct+repository.ProductVersion+repository.Name+ + repository.Architecture, isRepositoryConfigControl) >= 0 { + return nil, fmt.Errorf("repository display fields contain control characters") + } + gpgCheck := false + repoGPGCheck := revision.MetadataSigned && revision.RepomdSignaturePath == "repodata/repomd.xml.asc" && + strings.TrimSpace(revision.MetadataPublicKeyURL) != "" + switch repository.GPGPolicy { + case request.RepositoryGPGDisabled, request.RepositoryGPGAudit: + case request.RepositoryGPGVerifyPackage, request.RepositoryGPGEnforce: + if len(gpgKeyURLs) == 0 { + return nil, ErrRepositoryGPGKeyUnavailable + } + gpgCheck = true + default: + return nil, fmt.Errorf("unsupported repository GPG policy %q", repository.GPGPolicy) + } + repoID := "ocmanager-" + repository.PublishKey + enabled := 0 + if repository.Enabled { + enabled = 1 + } + content := fmt.Sprintf("[%s]\n"+ + "name=OCManager - %s %s - %s (%s)\n"+ + "baseurl=%s\n"+ + "enabled=%d\n"+ + "gpgcheck=%d\n"+ + "repo_gpgcheck=%d\n"+ + "metadata_expire=300\n"+ + "skip_if_unavailable=0\n", + repoID, repository.SystemProduct, repository.ProductVersion, repository.Name, + repository.Architecture, baseURL, enabled, boolInt(gpgCheck), boolInt(repoGPGCheck)) + if gpgCheck || repoGPGCheck { + content += "gpgkey=" + strings.Join(gpgKeyURLs, " ") + "\n" + } + return &request.GeneratedRepositoryConfig{ + RepositoryID: repository.ID, ActiveRevisionID: revision.ID, RepoID: repoID, + FileName: repoID + ".repo", ContentType: "text/plain; charset=utf-8", Content: content, + BaseURL: baseURL, RevisionURL: revisionURL, ImmutableRevision: immutableRevision, Enabled: repository.Enabled, + GPGCheck: gpgCheck, RepoGPGCheck: repoGPGCheck, GPGKeyURLs: append([]string(nil), gpgKeyURLs...), + }, nil +} + +func isRepositoryConfigControl(value rune) bool { + return unicode.IsControl(value) || value == '\u2028' || value == '\u2029' +} + +func boolInt(value bool) int { + if value { + return 1 + } + return 0 +} diff --git a/manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go b/manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go new file mode 100644 index 0000000000000000000000000000000000000000..88e146d706ace0f663653a8ede4d36c81a2c2162 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryContentService/repo_config_test.go @@ -0,0 +1,162 @@ +package repositoryContentService + +import ( + "errors" + "strings" + "testing" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +func TestBuildRepositoryConfigDisabledGPG(t *testing.T) { + publishedAt := time.Now().UTC() + repository := &po.SoftwareRepository{ + ID: "repository-id", SystemProduct: "OpenCloudOS", ProductVersion: "9.6", + Name: "private-apps", Architecture: request.RepositoryArchitectureX8664, + PublishKey: "publish-key", GPGPolicy: request.RepositoryGPGDisabled, + Enabled: true, Status: request.RepositoryStatusReady, ActiveRevisionID: "revision-id", + } + revision := &po.RepositoryRevision{ + ID: "revision-id", RepositoryID: repository.ID, + Status: request.RepositoryRevisionStatusPublished, PublishedAt: &publishedAt, + } + value, err := buildRepositoryConfig(repository, revision, + "https://repo.example.test/repositories/publish-key/current/", + "https://repo.example.test/repositories/publish-key/revisions/revision-id/", false, nil) + if err != nil { + t.Fatal(err) + } + for _, expected := range []string{ + "[ocmanager-publish-key]", "enabled=1", "gpgcheck=0", "repo_gpgcheck=0", + "baseurl=https://repo.example.test/repositories/publish-key/current/", + } { + if !strings.Contains(value.Content, expected+"\n") { + t.Fatalf("generated config does not contain %q:\n%s", expected, value.Content) + } + } + if value.ActiveRevisionID != revision.ID || value.RevisionURL == "" || + value.FileName != "ocmanager-publish-key.repo" || !value.Enabled || value.GPGCheck { + t.Fatalf("generated config metadata=%+v", value) + } +} + +func TestBuildRepositoryConfigReflectsDisabledRepository(t *testing.T) { + publishedAt := time.Now().UTC() + repository := &po.SoftwareRepository{ + ID: "repository-id", SystemProduct: "OpenCloudOS", ProductVersion: "9.6", Name: "private-apps", + Architecture: request.RepositoryArchitectureX8664, PublishKey: "publish-key", + GPGPolicy: request.RepositoryGPGDisabled, Status: request.RepositoryStatusReady, + ActiveRevisionID: "revision-id", + } + revision := &po.RepositoryRevision{ID: "revision-id", RepositoryID: repository.ID, + Status: request.RepositoryRevisionStatusPublished, PublishedAt: &publishedAt} + value, err := buildRepositoryConfig(repository, revision, "https://repo.test/current/", + "https://repo.test/revision/", false, nil) + if err != nil { + t.Fatal(err) + } + if value.Enabled || !strings.Contains(value.Content, "\nenabled=0\n") { + t.Fatalf("disabled repository config=%+v", value) + } +} + +func TestBuildRepositoryConfigFailsClosedForGPG(t *testing.T) { + publishedAt := time.Now().UTC() + repository := &po.SoftwareRepository{ + ID: "repository-id", SystemProduct: "OpenCloudOS", ProductVersion: "9.6", Name: "signed", + Architecture: request.RepositoryArchitectureX8664, PublishKey: "publish-key", + GPGPolicy: request.RepositoryGPGVerifyPackage, Enabled: true, + Status: request.RepositoryStatusReady, ActiveRevisionID: "revision-id", + } + revision := &po.RepositoryRevision{ID: "revision-id", RepositoryID: repository.ID, + Status: request.RepositoryRevisionStatusPublished, PublishedAt: &publishedAt} + if _, err := buildRepositoryConfig(repository, revision, "https://repo.test/current/", + "https://repo.test/revision/", false, nil); !errors.Is(err, ErrRepositoryGPGKeyUnavailable) { + t.Fatalf("VERIFY_PACKAGE without a public key error=%v", err) + } + keyURL := "https://repo.test/publish-key/keys/FINGERPRINT/public.key" + value, err := buildRepositoryConfig(repository, revision, "https://repo.test/current/", + "https://repo.test/revision/", false, []string{keyURL}) + if err != nil || !value.GPGCheck || value.RepoGPGCheck || len(value.GPGKeyURLs) != 1 || + !strings.Contains(value.Content, "gpgcheck=1\n") || !strings.Contains(value.Content, "gpgkey="+keyURL+"\n") { + t.Fatalf("VERIFY_PACKAGE config=%+v err=%v", value, err) + } + revision.Status = request.RepositoryRevisionStatusReady + if _, err := buildRepositoryConfig(repository, revision, "https://repo.test/current/", + "https://repo.test/revision/", false, []string{keyURL}); !errors.Is(err, ErrRepositoryNotPublished) { + t.Fatalf("unpublished revision error=%v", err) + } +} + +func TestBuildRepositoryConfigTruthfullyEnablesMetadataVerification(t *testing.T) { + publishedAt := time.Now().UTC() + repository := &po.SoftwareRepository{ + ID: "repository-id", SystemProduct: "OpenCloudOS", ProductVersion: "9.6", Name: "signed-metadata", + Architecture: request.RepositoryArchitectureX8664, PublishKey: "publish-key", + GPGPolicy: request.RepositoryGPGAudit, Enabled: true, + Status: request.RepositoryStatusReady, ActiveRevisionID: "revision-id", + } + revision := &po.RepositoryRevision{ID: "revision-id", RepositoryID: repository.ID, + Status: request.RepositoryRevisionStatusPublished, PublishedAt: &publishedAt, + MetadataSigned: true, RepomdSignaturePath: "repodata/repomd.xml.asc", + MetadataPublicKeyURL: "https://repo.test/repositories/publish-key/revisions/revision-id/keys/metadata.key"} + value, err := buildRepositoryConfig(repository, revision, "https://repo.test/current/", + "https://repo.test/revision/", false, []string{revision.MetadataPublicKeyURL}) + if err != nil || value.GPGCheck || !value.RepoGPGCheck || + !strings.Contains(value.Content, "gpgcheck=0\n") || + !strings.Contains(value.Content, "repo_gpgcheck=1\n") || + !strings.Contains(value.Content, "gpgkey="+revision.MetadataPublicKeyURL+"\n") { + t.Fatalf("metadata signed config=%+v err=%v", value, err) + } + revision.MetadataPublicKeyURL = "" + value, err = buildRepositoryConfig(repository, revision, "https://repo.test/current/", + "https://repo.test/revision/", false, nil) + if err != nil || value.RepoGPGCheck || !strings.Contains(value.Content, "repo_gpgcheck=0\n") { + t.Fatalf("incomplete metadata signature must not enable repo_gpgcheck: value=%+v err=%v", value, err) + } +} + +func TestBuildRepositoryConfigRejectsUnsafeINIData(t *testing.T) { + publishedAt := time.Now().UTC() + repository := &po.SoftwareRepository{ + ID: "repository-id", SystemProduct: "OpenCloudOS", ProductVersion: "9.6", Name: "private", + Architecture: request.RepositoryArchitectureX8664, PublishKey: "publish-key]\\ninjected", + GPGPolicy: request.RepositoryGPGDisabled, Enabled: true, + Status: request.RepositoryStatusReady, ActiveRevisionID: "revision-id", + } + revision := &po.RepositoryRevision{ID: "revision-id", RepositoryID: repository.ID, + Status: request.RepositoryRevisionStatusPublished, PublishedAt: &publishedAt} + if _, err := buildRepositoryConfig(repository, revision, "https://repo.test/current/", + "https://repo.test/revision/", false, nil); err == nil { + t.Fatal("unsafe publish key was accepted") + } + repository.PublishKey = "publish-key" + repository.SystemProduct = "OpenCloudOS\u2028baseurl=https://attacker.invalid" + if _, err := buildRepositoryConfig(repository, revision, "https://repo.test/current/", + "https://repo.test/revision/", false, nil); err == nil { + t.Fatal("unsafe display value was accepted") + } +} + +func TestRemotePublicationURLsBindCurrentAndImmutableRevision(t *testing.T) { + current, revision, err := remotePublicationURLs("https://repo.example.test/repositories/", + "oc9-private", "11111111-1111-4111-8111-111111111111") + if err != nil { + t.Fatal(err) + } + if current != "https://repo.example.test/repositories/oc9-private/current" || + revision != "https://repo.example.test/repositories/oc9-private/revisions/11111111-1111-4111-8111-111111111111" { + t.Fatalf("remote publication URLs = %q / %q", current, revision) + } + for _, baseURL := range []string{ + "file:///repositories", "https://user:password@repo.example.test/repositories", + "https://repo.example.test/repositories?revision=latest", + } { + if _, _, err = remotePublicationURLs(baseURL, "oc9-private", + "11111111-1111-4111-8111-111111111111"); err == nil { + t.Fatalf("unsafe remote publication base URL %q was accepted", baseURL) + } + } +} diff --git a/manager/backend/services/footstone/domain/repositoryContentService/report.go b/manager/backend/services/footstone/domain/repositoryContentService/report.go new file mode 100644 index 0000000000000000000000000000000000000000..562a64edd12e5e21be98b20ff8a3fdce637249aa --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryContentService/report.go @@ -0,0 +1,283 @@ +package repositoryContentService + +import ( + "fmt" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryDiagnosisService" +) + +type inventoryGPGKey struct { + KeyID string `json:"key_id"` + Fingerprint string `json:"fingerprint"` + PublicKeyURL string `json:"public_key_url"` + CreatedAt time.Time `json:"created_at"` +} + +type inventoryPackage struct { + NEVRA string `json:"nevra"` + FileName string `json:"file_name"` + Architecture string `json:"architecture"` + SizeBytes int64 `json:"size_bytes"` + SHA256 string `json:"sha256"` + Source string `json:"source"` + CreatedAt time.Time `json:"created_at"` +} + +type inventoryRevision struct { + ID string `json:"id"` + Kind string `json:"kind"` + Status string `json:"status"` + PackageCount int `json:"package_count"` + TotalBytes int64 `json:"total_bytes"` + ManifestSHA256 string `json:"manifest_sha256"` + RepomdSHA256 string `json:"repomd_sha256"` + SnapshotManifestSHA256 string `json:"snapshot_manifest_sha256,omitempty"` + CreatedAt time.Time `json:"created_at"` + PublishedAt *time.Time `json:"published_at,omitempty"` +} + +type inventoryRepository struct { + ID string `json:"id"` + SystemProduct string `json:"system_product"` + ProductVersion string `json:"product_version"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Architecture string `json:"architecture"` + Type string `json:"type"` + UpstreamURL string `json:"upstream_url,omitempty"` + AllowUpload bool `json:"allow_upload"` + IncludePackages []string `json:"include_packages"` + ExcludePackages []string `json:"exclude_packages"` + GPGPolicy string `json:"gpg_policy"` + Enabled bool `json:"enabled"` + Status string `json:"status"` + ActiveRevisionID string `json:"active_revision_id,omitempty"` + QuarantineCount int64 `json:"quarantine_count"` + ActiveRevision *inventoryRevision `json:"active_revision,omitempty"` + Packages []inventoryPackage `json:"packages"` + AuthorizedGPGKeys []inventoryGPGKey `json:"authorized_gpg_keys"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type validationTask struct { + ID string `json:"id"` + RootTaskID string `json:"root_task_id"` + RevisionID string `json:"revision_id,omitempty"` + Status string `json:"status"` + Attempt int `json:"attempt"` + CurrentStep string `json:"current_step,omitempty"` + ErrorCode string `json:"error_code,omitempty"` + ErrorMessage string `json:"error_message,omitempty"` + DurationMillis *int64 `json:"duration_millis,omitempty"` + Diagnosis *po.RepositoryDiagnosis `json:"diagnosis,omitempty"` + CreatedAt time.Time `json:"created_at"` + StartedAt *time.Time `json:"started_at,omitempty"` + FinishedAt *time.Time `json:"finished_at,omitempty"` +} + +type repositoryLister interface { + List(entity.Context, request.ListSoftwareRepositoriesRequest) ([]po.SoftwareRepository, request.PageResponse, error) +} + +func (s *service) ExportInventory(ctx entity.Context, projectID int64) (*request.RepositoryExport, error) { + repositories, err := s.listAllRepositories(ctx, projectID) + if err != nil { + return nil, err + } + items := make([]inventoryRepository, 0, len(repositories)) + var packageCount, totalBytes, keyCount int64 + for index := range repositories { + repository := &repositories[index] + include, exclude, decodeErr := repository.PackageFilters() + if decodeErr != nil { + return nil, decodeErr + } + item := inventoryRepository{ + ID: repository.ID, SystemProduct: repository.SystemProduct, + ProductVersion: repository.ProductVersion, Name: repository.Name, + Description: repository.Description, Architecture: repository.Architecture, + Type: repository.Type, UpstreamURL: repository.UpstreamURL, AllowUpload: repository.AllowUpload, + IncludePackages: include, ExcludePackages: exclude, GPGPolicy: repository.GPGPolicy, + Enabled: repository.Enabled, Status: repository.Status, + ActiveRevisionID: repository.ActiveRevisionID, Packages: []inventoryPackage{}, + AuthorizedGPGKeys: []inventoryGPGKey{}, CreatedAt: repository.CreatedAt, UpdatedAt: repository.UpdatedAt, + } + _, quarantinePage, listErr := s.content.ListQuarantine(request.ListRepositoryQuarantineRequest{ + RepositoryID: repository.ID, PageRequest: request.PageRequest{PageNumber: 1, PageSize: 1}, + }, nil) + if listErr != nil { + return nil, listErr + } + item.QuarantineCount = quarantinePage.Count + if repository.ActiveRevisionID != "" { + revision, revisionErr := s.content.GetRevision(repository.ID, repository.ActiveRevisionID, nil) + if revisionErr != nil { + return nil, revisionErr + } + item.ActiveRevision = inventoryRevisionFromPO(revision) + packages, packagesErr := s.content.PackagesByRevision(revision.ID, nil) + if packagesErr != nil { + return nil, packagesErr + } + for packageIndex := range packages { + value := &packages[packageIndex] + item.Packages = append(item.Packages, inventoryPackage{ + NEVRA: value.NEVRA, FileName: value.FileName, Architecture: value.Architecture, + SizeBytes: value.SizeBytes, SHA256: value.SHA256, Source: value.Source, CreatedAt: value.CreatedAt, + }) + packageCount++ + totalBytes += value.SizeBytes + } + } + keys, keysErr := s.ListGPGKeys(ctx, repository.ID) + if keysErr != nil { + return nil, keysErr + } + for keyIndex := range keys { + key := &keys[keyIndex] + item.AuthorizedGPGKeys = append(item.AuthorizedGPGKeys, inventoryGPGKey{ + KeyID: key.KeyID, Fingerprint: key.Fingerprint, + PublicKeyURL: key.PublicKeyURL, CreatedAt: key.CreatedAt, + }) + keyCount++ + } + items = append(items, item) + } + report := struct { + SchemaVersion string `json:"schema_version"` + GeneratedAt time.Time `json:"generated_at"` + ProjectID int64 `json:"project_id"` + Repositories []inventoryRepository `json:"repositories"` + Summary map[string]int64 `json:"summary"` + }{ + SchemaVersion: "ocmanager.repository.inventory/v1", GeneratedAt: s.now(), ProjectID: projectID, + Repositories: items, Summary: map[string]int64{ + "repositories": int64(len(items)), "packages": packageCount, + "package_bytes": totalBytes, "authorized_gpg_keys": keyCount, + }, + } + return request.NewRepositoryJSONExport(fmt.Sprintf("repository-inventory-project-%d.json", projectID), report) +} + +func (s *service) ExportValidationReport(ctx entity.Context, repositoryID string) (*request.RepositoryExport, error) { + repository, err := s.repositories.Describe(ctx, repositoryID) + if err != nil { + return nil, err + } + allTasks := make([]po.RepositoryTask, 0) + for pageNumber := 1; ; pageNumber++ { + values, page, listErr := s.tasks.List(ctx, request.ListRepositoryTasksRequest{ + RepositoryID: repository.ID, PageRequest: request.PageRequest{PageNumber: pageNumber, PageSize: 200}, + }) + if listErr != nil { + return nil, listErr + } + allTasks = append(allTasks, values...) + if pageNumber >= page.PageCount { + break + } + } + validations := make([]validationTask, 0) + statuses := make([]string, 0) + for index := range allTasks { + value := &allTasks[index] + if value.Type != request.RepositoryTaskTypeValidate { + continue + } + item := validationTask{ + ID: value.ID, RootTaskID: value.RootTaskID, RevisionID: value.RevisionID, + Status: value.Status, Attempt: value.Attempt, + CurrentStep: repositoryDiagnosisService.Sanitize(value.CurrentStep), + ErrorCode: value.ErrorCode, ErrorMessage: repositoryDiagnosisService.Sanitize(value.ErrorMessage), + Diagnosis: value.StructuredDiagnosis(), CreatedAt: value.CreatedAt, + StartedAt: value.StartedAt, FinishedAt: value.FinishedAt, + } + if value.StartedAt != nil && value.FinishedAt != nil { + duration := value.FinishedAt.Sub(*value.StartedAt).Milliseconds() + item.DurationMillis = &duration + } + validations = append(validations, item) + statuses = append(statuses, value.Status) + } + status, passed, failed, running := validationReportStatus(statuses) + report := struct { + SchemaVersion string `json:"schema_version"` + GeneratedAt time.Time `json:"generated_at"` + RepositoryID string `json:"repository_id"` + SystemProduct string `json:"system_product"` + ProductVersion string `json:"product_version"` + RepositoryName string `json:"repository_name"` + ActiveRevisionID string `json:"active_revision_id,omitempty"` + Status string `json:"status"` + ValidationRuns []validationTask `json:"validation_runs"` + Summary map[string]int `json:"summary"` + }{ + SchemaVersion: "ocmanager.repository.validation/v1", GeneratedAt: s.now(), + RepositoryID: repository.ID, SystemProduct: repository.SystemProduct, + ProductVersion: repository.ProductVersion, RepositoryName: repository.Name, + ActiveRevisionID: repository.ActiveRevisionID, Status: status, ValidationRuns: validations, + Summary: map[string]int{"total": len(validations), "passed": passed, "failed": failed, "in_progress": running}, + } + return request.NewRepositoryJSONExport(fmt.Sprintf("repository-validation-%s.json", repository.ID), report) +} + +func (s *service) listAllRepositories(ctx entity.Context, projectID int64) ([]po.SoftwareRepository, error) { + lister, ok := s.repositories.(repositoryLister) + if !ok { + return nil, fmt.Errorf("repository inventory listing is unavailable") + } + values := make([]po.SoftwareRepository, 0) + for pageNumber := 1; ; pageNumber++ { + pageValues, page, err := lister.List(ctx, request.ListSoftwareRepositoriesRequest{ + ProjectID: projectID, PageRequest: request.PageRequest{PageNumber: pageNumber, PageSize: 200}, + }) + if err != nil { + return nil, err + } + values = append(values, pageValues...) + if pageNumber >= page.PageCount { + return values, nil + } + } +} + +func inventoryRevisionFromPO(value *po.RepositoryRevision) *inventoryRevision { + if value == nil { + return nil + } + return &inventoryRevision{ + ID: value.ID, Kind: value.Kind, Status: value.Status, PackageCount: value.PackageCount, + TotalBytes: value.TotalBytes, ManifestSHA256: value.ManifestSHA256, RepomdSHA256: value.RepomdSHA256, + SnapshotManifestSHA256: value.SnapshotManifestSHA256, CreatedAt: value.CreatedAt, + PublishedAt: value.PublishedAt, + } +} + +func validationReportStatus(statuses []string) (status string, passed, failed, running int) { + if len(statuses) == 0 { + return "NOT_RUN", 0, 0, 0 + } + for _, value := range statuses { + switch value { + case request.RepositoryTaskStatusSuccess: + passed++ + case request.RepositoryTaskStatusPending, request.RepositoryTaskStatusRunning, + request.RepositoryTaskStatusCancelRequested: + running++ + default: + failed++ + } + } + if failed > 0 { + return "FAIL", passed, failed, running + } + if running > 0 { + return "IN_PROGRESS", passed, failed, running + } + return "PASS", passed, failed, running +} diff --git a/manager/backend/services/footstone/domain/repositoryContentService/report_integration_test.go b/manager/backend/services/footstone/domain/repositoryContentService/report_integration_test.go new file mode 100644 index 0000000000000000000000000000000000000000..7dfb2cb84142602109bb5f5ff86acc3bb6d4b5ec --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryContentService/report_integration_test.go @@ -0,0 +1,224 @@ +//go:build integration + +package repositoryContentService + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/google/uuid" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + baseconfig "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + ftpb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/footstone" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorycontentrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositoryrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorytaskrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryTaskService" + appconfig "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" +) + +func TestRepositoryInventoryAndValidationExportsWithMySQL(t *testing.T) { + dsn := os.Getenv("OCM_REPOSITORY_REPORT_DSN") + if dsn == "" { + t.Skip("OCM_REPOSITORY_REPORT_DSN is not set") + } + db, err := database.NewDBClient(baseconfig.AccessPoint{Source: dsn}) + if err != nil { + t.Fatal(err) + } + defer db.Close() + if err = db.DB().AutoMigrate(&po.Project{}); err != nil { + t.Fatal(err) + } + repositories := repositoryrepo.New(db) + taskRepository := repositorytaskrepo.New(db) + taskTargetRepository := repositorytaskrepo.NewTargetRepository(db) + contentRepository := repositorycontentrepo.New(db) + for _, migrate := range []func() error{ + repositories.SyncTables, taskRepository.SyncTables, taskTargetRepository.SyncTables, contentRepository.SyncTables, + } { + if err = migrate(); err != nil { + t.Fatal(err) + } + } + const projectID int64 = 900011 + repositoryID, revisionID := uuid.NewString(), uuid.NewString() + cleanupRepositoryReportRows(t, db, projectID, repositoryID) + defer cleanupRepositoryReportRows(t, db, projectID, repositoryID) + if err = db.DB().Create(&po.Project{ + Base: po.Base{ID: projectID}, Name: "repository-phase11", EnName: "repository-phase11", Creator: "admin", + }).Error; err != nil { + t.Fatal(err) + } + now := time.Date(2026, 8, 24, 10, 0, 0, 0, time.UTC) + repository := &po.SoftwareRepository{ + ID: repositoryID, ProjectID: projectID, SystemProduct: "OpenCloudOS", ProductVersion: "9.6", + Name: "phase11-report", Architecture: request.RepositoryArchitectureX8664, + Type: request.RepositoryTypePrivate, StorageLocation: "public/phase11", PublishKey: "phase11-report", + AllowUpload: true, GPGPolicy: request.RepositoryGPGDisabled, Enabled: true, + Status: request.RepositoryStatusReady, ActiveRevisionID: revisionID, Creator: "admin", + CreatedAt: now, UpdatedAt: now, + } + if err = repository.SetPackageFilters([]string{"ocm-*"}, []string{"*-debug"}); err != nil { + t.Fatal(err) + } + if err = repositories.Create(repository, nil); err != nil { + t.Fatal(err) + } + publishedAt := now.Add(time.Minute) + packageSHA := sha256.Sum256([]byte("phase11-package")) + revision := &po.RepositoryRevision{ + ID: revisionID, RepositoryID: repositoryID, TaskID: uuid.NewString(), + Kind: request.RepositoryRevisionKindUpload, Status: request.RepositoryRevisionStatusPublished, + PackageCount: 1, TotalBytes: 15, ManifestSHA256: hex.EncodeToString(packageSHA[:]), + RepomdSHA256: hex.EncodeToString(packageSHA[:]), RelativePath: "public/phase11/revisions/" + revisionID, + CreatedBy: "admin", CreatedAt: now, PublishedAt: &publishedAt, + } + packages := []po.RepositoryPackage{{ + ID: uuid.NewString(), RepositoryID: repositoryID, RevisionID: revisionID, + Name: "ocm-phase11", Epoch: "0", Version: "1.0", Release: "1", + Architecture: request.RepositoryArchitectureNoarch, + NEVRA: "ocm-phase11-0:1.0-1.noarch", FileName: "ocm-phase11-1.0-1.noarch.rpm", + RelativePath: "ocm-phase11-1.0-1.noarch.rpm", SizeBytes: 15, + SHA256: hex.EncodeToString(packageSHA[:]), Source: request.RepositoryPackageSourceUpload, CreatedAt: now, + }} + if err = contentRepository.CreateRevision(revision, packages, nil); err != nil { + t.Fatal(err) + } + if err = contentRepository.CreateQuarantine(&po.RepositoryQuarantineItem{ + ID: uuid.NewString(), RepositoryID: repositoryID, TaskID: uuid.NewString(), + OriginalName: "damaged.rpm", RelativePath: "quarantine/damaged.rpm", SizeBytes: 4, + SHA256: hex.EncodeToString(packageSHA[:]), ReasonCode: request.RepositoryQuarantineDamagedRPM, + ReasonMessage: "fixture", CreatedAt: now, + }, nil); err != nil { + t.Fatal(err) + } + startedAt, finishedAt := now.Add(2*time.Minute), now.Add(2*time.Minute+1500*time.Millisecond) + diagnosedAt := finishedAt + evidence, _ := json.Marshal([]string{"dnf exit code: 1"}) + for _, task := range []*po.RepositoryTask{ + { + ID: uuid.NewString(), RootTaskID: uuid.NewString(), RepositoryID: repositoryID, + ProjectID: projectID, RevisionID: revisionID, Type: request.RepositoryTaskTypeValidate, + Status: request.RepositoryTaskStatusSuccess, MaxAttempts: 1, RequestedBy: "admin", + AvailableAt: now, StartedAt: &startedAt, FinishedAt: &finishedAt, PayloadJSON: "{}", + CreatedAt: now, UpdatedAt: now, + }, + { + ID: uuid.NewString(), RootTaskID: uuid.NewString(), RepositoryID: repositoryID, + ProjectID: projectID, RevisionID: revisionID, Type: request.RepositoryTaskTypeValidate, + Status: request.RepositoryTaskStatusFailed, MaxAttempts: 1, RequestedBy: "admin", + AvailableAt: now, StartedAt: &startedAt, FinishedAt: &finishedAt, + ErrorCode: "DNF_VALIDATION_FAILED", ErrorMessage: "dependency missing token=secret", + FailureCategory: "DEPENDENCY", FailedStep: "dnf install", LikelyCause: "dependency unavailable", + DiagnosisEvidenceJSON: evidence, RecommendedAction: "publish dependency", + DiagnosisRuleID: "DNF_DEPENDENCY_MISSING", DiagnosisGeneratedAt: &diagnosedAt, + PayloadJSON: "{}", CreatedAt: now.Add(time.Second), UpdatedAt: now.Add(time.Second), + }, + } { + task.RootTaskID = task.ID + if err = taskRepository.Create(task, nil); err != nil { + t.Fatal(err) + } + } + manager := repositoryService.New(db) + tasks := repositoryTaskService.New(db) + store, err := storage.New(appconfig.RepositoryConfig{Root: t.TempDir(), PublicBaseURL: "http://repository.invalid"}) + if err != nil { + t.Fatal(err) + } + service := New(db, manager, tasks, store) + service.now = func() time.Time { return now.Add(time.Hour) } + ctx := entity.NewDefaultContext() + ctx.User = &ftpb.UserInfo{LoginName: "admin", Roles: []*ftpb.ModuleRole{{RoleId: request.DefaultGlobalRoleName}}} + + inventory, err := service.ExportInventory(ctx, projectID) + if err != nil { + t.Fatal(err) + } + assertRepositoryExport(t, inventory, "ocmanager.repository.inventory/v1", 1) + validation, err := service.ExportValidationReport(ctx, repositoryID) + if err != nil { + t.Fatal(err) + } + assertRepositoryExport(t, validation, "ocmanager.repository.validation/v1", 2) + var validationJSON struct { + Status string `json:"status"` + Summary map[string]int `json:"summary"` + } + if err = json.Unmarshal([]byte(validation.Content), &validationJSON); err != nil { + t.Fatal(err) + } + if validationJSON.Status != "FAIL" || validationJSON.Summary["passed"] != 1 || validationJSON.Summary["failed"] != 1 { + t.Fatalf("validation report=%s", validation.Content) + } + writeRepositoryReportFixture(t, inventory) + writeRepositoryReportFixture(t, validation) + if validation.Content == "" || !json.Valid([]byte(validation.Content)) || + strings.Contains(validation.Content, "token=secret") { + t.Fatalf("validation report is invalid or contains an unredacted secret") + } + t.Logf("inventory_sha256=%s validation_sha256=%s validation_status=%s", + inventory.SHA256, validation.SHA256, validationJSON.Status) +} + +func writeRepositoryReportFixture(t *testing.T, value *request.RepositoryExport) { + t.Helper() + output := os.Getenv("OCM_REPOSITORY_REPORT_OUTPUT") + if output == "" { + return + } + if err := os.MkdirAll(output, 0750); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(output, value.FileName), []byte(value.Content), 0640); err != nil { + t.Fatal(err) + } +} + +func assertRepositoryExport(t *testing.T, value *request.RepositoryExport, schema string, expectedTotal int) { + t.Helper() + digest := sha256.Sum256([]byte(value.Content)) + if value.ContentType != "application/json" || value.SHA256 != hex.EncodeToString(digest[:]) { + t.Fatalf("invalid export metadata: %#v", value) + } + var report struct { + SchemaVersion string `json:"schema_version"` + Summary map[string]int `json:"summary"` + } + if err := json.Unmarshal([]byte(value.Content), &report); err != nil { + t.Fatal(err) + } + if report.SchemaVersion != schema || report.Summary["repositories"]+report.Summary["total"] != expectedTotal { + t.Fatalf("report schema/summary=%s/%v", report.SchemaVersion, report.Summary) + } +} + +func cleanupRepositoryReportRows(t *testing.T, db *database.DBClient, projectID int64, repositoryID string) { + t.Helper() + for _, model := range []interface{}{ + new(po.RepositoryTask), new(po.RepositoryQuarantineItem), new(po.RepositoryPackage), + new(po.RepositoryRevision), new(po.RepositoryGPGKey), new(po.RepositoryUpload), new(po.SoftwareRepository), + } { + query := db.DB().Unscoped() + if _, ok := model.(*po.SoftwareRepository); ok { + query = query.Where("id = ?", repositoryID) + } else { + query = query.Where("repository_id = ?", repositoryID) + } + _ = query.Delete(model).Error + } + _ = db.DB().Unscoped().Where("id = ?", projectID).Delete(&po.Project{}).Error +} diff --git a/manager/backend/services/footstone/domain/repositoryContentService/report_test.go b/manager/backend/services/footstone/domain/repositoryContentService/report_test.go new file mode 100644 index 0000000000000000000000000000000000000000..2dbf407bc2dff32e87c9ca21510271c5299e78ed --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryContentService/report_test.go @@ -0,0 +1,29 @@ +package repositoryContentService + +import ( + "testing" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" +) + +func TestValidationReportStatus(t *testing.T) { + tests := []struct { + name string + statuses []string + status string + passed, failed, run int + }{ + {name: "not run", status: "NOT_RUN"}, + {name: "pass", statuses: []string{request.RepositoryTaskStatusSuccess}, status: "PASS", passed: 1}, + {name: "running", statuses: []string{request.RepositoryTaskStatusSuccess, request.RepositoryTaskStatusRunning}, status: "IN_PROGRESS", passed: 1, run: 1}, + {name: "failed", statuses: []string{request.RepositoryTaskStatusFailed, request.RepositoryTaskStatusPartialSuccess}, status: "FAIL", failed: 2}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + status, passed, failed, running := validationReportStatus(test.statuses) + if status != test.status || passed != test.passed || failed != test.failed || running != test.run { + t.Fatalf("status/counts=%s/%d/%d/%d", status, passed, failed, running) + } + }) + } +} diff --git a/manager/backend/services/footstone/domain/repositoryContentService/service.go b/manager/backend/services/footstone/domain/repositoryContentService/service.go new file mode 100644 index 0000000000000000000000000000000000000000..b3f6677f3eb702c11a1ecdd83420c95728352f6f --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryContentService/service.go @@ -0,0 +1,1010 @@ +package repositoryContentService + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "path/filepath" + "regexp" + "strings" + "time" + "unicode" + "unicode/utf8" + + "github.com/google/uuid" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorycontentrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorypackageinstallrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositoryrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorytaskrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryContentService/internal" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryTaskService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" +) + +var ( + ErrUploadDisabled = errors.New("repository does not allow private uploads") + ErrUploadNotReceiving = errors.New("repository upload is not receiving data") + ErrUploadIncomplete = errors.New("repository upload is incomplete") + ErrUploadChecksum = errors.New("repository upload checksum mismatch") + ErrUploadExpired = errors.New("repository upload has expired") + ErrUploadOwnerMismatch = errors.New("repository upload belongs to another user") + ErrUploadChunkConflict = errors.New("repository upload chunk conflicts with the stored chunk") + ErrDeletionPreviewChanged = errors.New("PREVIEW_CHANGED: repository package deletion preview changed") + ErrDeletionPreviewExpired = errors.New("PREVIEW_EXPIRED: repository package deletion preview expired") + ErrDeletionPreviewForbidden = errors.New("PREVIEW_FORBIDDEN: repository package deletion preview belongs to another user") + ErrDeletionRevisionChanged = errors.New("REVISION_CHANGED: repository revision changed after package deletion preview") + ErrSyncUnavailable = errors.New("repository is disabled or has no upstream URL") + ErrSyncAlreadyRunning = errors.New("repository already has a synchronization task in progress") + ErrSystemRepositorySync = errors.New("system-managed Node RPM repository cannot use remote synchronization") + sha256Pattern = regexp.MustCompile(`^[0-9a-f]{64}$`) +) + +type repositoryManager interface { + Describe(entity.Context, string) (*po.SoftwareRepository, error) +} + +type repositoryNodeManager interface { + DescribeInternal(string) (*po.RepositoryStorageNode, error) +} + +type repositoryUploadTaskReader interface { + GetByID(string, entity.Session) (*po.RepositoryTask, error) +} + +type remoteUploadStore interface { + BeginUpload(context.Context, *po.RepositoryStorageNode, string, int64) (string, error) + AppendUploadChunk(context.Context, *po.RepositoryStorageNode, string, int64, int64, []byte) (int64, error) + PutUploadChunk(context.Context, *po.RepositoryStorageNode, string, int, []byte, string) (string, error) + CompleteUpload(context.Context, *po.RepositoryStorageNode, string, int64, string, ...int) (string, int64, string, error) + AbortUpload(context.Context, *po.RepositoryStorageNode, string) error + PutGPGPublicKey(context.Context, *po.RepositoryStorageNode, string, string, string, string, []byte) (string, string, error) + VerifyGPGPublicKey(context.Context, *po.RepositoryStorageNode, string, string, string, string) (string, error) + RemoveGPGPublicKey(context.Context, *po.RepositoryStorageNode, string, string, string) error +} + +type dependencyPreviewer interface { + Preview(context.Context, string, string, []string) ([]request.RepositoryDependencyImpact, []string, string, error) +} + +type Service = *service + +type service struct { + db entity.SessionFactory + repositories repositoryManager + tasks repositoryTaskService.Service + content repositorycontentrepo.Repository + uploadTasks repositoryUploadTaskReader + previews repositorypackageinstallrepo.Repository + repositoryRecords repositoryrepo.Repository + store *storage.Store + nodes repositoryNodeManager + remoteUpload remoteUploadStore + dependencyPreview dependencyPreviewer + now func() time.Time +} + +func New(db *database.DBClient, repositories repositoryManager, tasks repositoryTaskService.Service, + store *storage.Store, +) Service { + return &service{ + db: db, repositories: repositories, tasks: tasks, + content: repositorycontentrepo.New(db), uploadTasks: repositorytaskrepo.New(db), + repositoryRecords: repositoryrepo.New(db), store: store, + previews: repositorypackageinstallrepo.New(db), dependencyPreview: newDNFDependencyPreviewer(), + now: func() time.Time { return time.Now().UTC() }, + } +} + +func (s *service) ConfigureDependencyPreviewer(previewer dependencyPreviewer) { + s.dependencyPreview = previewer +} + +func (s *service) SyncTables() error { return s.content.SyncTables() } + +func (s *service) ConfigureRemoteUploads(nodes repositoryNodeManager, remote remoteUploadStore) { + s.nodes, s.remoteUpload = nodes, remote +} + +// ImportRemoteRevision writes repository-node result metadata in the caller's task transaction. +// Repeated Prepare calls are accepted only when the immutable metadata is identical. +func (s *service) ImportRemoteRevision(revision *po.RepositoryRevision, packages []po.RepositoryPackage, + quarantine []po.RepositoryQuarantineItem, session entity.Session, +) error { + if revision == nil || session == nil { + return errors.New("remote repository revision and transaction are required") + } + existing, err := s.content.GetRevision(revision.RepositoryID, revision.ID, session) + if err == nil { + storedPackages, listErr := s.content.PackagesByRevision(revision.ID, session) + if listErr != nil { + return listErr + } + storedQuarantine, listErr := s.content.QuarantineByTask(revision.TaskID, session) + if listErr != nil { + return listErr + } + if !sameRemoteRevision(existing, revision) || !sameRemotePackages(storedPackages, packages) || + !sameRemoteQuarantine(storedQuarantine, quarantine) { + return errors.New("remote repository revision metadata does not match the prepared result") + } + return nil + } + if !errors.Is(err, repositorycontentrepo.ErrRevisionNotFound) { + return err + } + if err = s.content.CreateRevision(revision, packages, session); err != nil { + return err + } + for index := range quarantine { + if err = s.content.CreateQuarantine(&quarantine[index], session); err != nil { + return err + } + } + return nil +} + +// ApplyRemoteUploadOutcome completes the control-plane upload record in the same transaction +// that imports the immutable revision and quarantine metadata. +func (s *service) ApplyRemoteUploadOutcome(task *po.RepositoryTask, snapshot request.RepositoryUploadSnapshot, + completionStatus, errorCode, errorMessage string, session entity.Session, +) error { + if task == nil || session == nil || task.Type != request.RepositoryTaskTypeUpload { + return errors.New("remote upload task and transaction are required") + } + upload, err := s.content.GetUpload(snapshot.UploadID, true, session) + if err != nil { + return err + } + if upload.RepositoryID != task.RepositoryID || upload.StorageNodeID != task.StorageNodeID || + !uploadTaskMatches(upload.TaskID, task) || upload.FileName != snapshot.FileName || upload.Kind != snapshot.Kind || + upload.RelativePath != snapshot.RelativePath || upload.ExpectedSize != snapshot.SizeBytes || + upload.ActualSHA256 != snapshot.SHA256 { + return errors.New("remote upload metadata does not match the task snapshot") + } + if completionStatus != request.RepositoryTaskStatusSuccess && + completionStatus != request.RepositoryTaskStatusPartialSuccess { + return errors.New("remote upload completion status is invalid") + } + if upload.Status == request.RepositoryUploadStatusCompleted { + if upload.ErrorCode == errorCode && upload.ErrorMessage == errorMessage { + return nil + } + return errors.New("remote upload outcome does not match the stored result") + } + if upload.Status != request.RepositoryUploadStatusReady { + return errors.New("remote upload is not ready for result import") + } + upload.Status = request.RepositoryUploadStatusCompleted + upload.ErrorCode = errorCode + upload.ErrorMessage = errorMessage + return s.content.UpdateUpload(upload, session) +} + +// ImportRemoteUploadFailure records rejected objects without creating or activating a revision. +func (s *service) ImportRemoteUploadFailure(task *po.RepositoryTask, snapshot request.RepositoryUploadSnapshot, + quarantine []po.RepositoryQuarantineItem, errorCode, errorMessage string, session entity.Session, +) error { + if task == nil || session == nil || task.Type != request.RepositoryTaskTypeUpload || len(quarantine) == 0 { + return errors.New("remote upload failure metadata and transaction are required") + } + stored, err := s.content.QuarantineByTask(task.ID, session) + if err != nil { + return err + } + if len(stored) > 0 { + if !sameRemoteQuarantine(stored, quarantine) { + return errors.New("remote upload quarantine does not match the stored result") + } + } else { + for index := range quarantine { + if err = s.content.CreateQuarantine(&quarantine[index], session); err != nil { + return err + } + } + } + upload, err := s.content.GetUpload(snapshot.UploadID, true, session) + if err != nil { + return err + } + if upload.RepositoryID != task.RepositoryID || upload.StorageNodeID != task.StorageNodeID || + !uploadTaskMatches(upload.TaskID, task) || upload.FileName != snapshot.FileName || upload.Kind != snapshot.Kind || + upload.RelativePath != snapshot.RelativePath || upload.ExpectedSize != snapshot.SizeBytes || + upload.ActualSHA256 != snapshot.SHA256 { + return errors.New("failed remote upload metadata does not match the task snapshot") + } + if upload.Status == request.RepositoryUploadStatusFailed { + if upload.ErrorCode == errorCode && upload.ErrorMessage == errorMessage { + return nil + } + return errors.New("remote upload failure does not match the stored outcome") + } + if upload.Status != request.RepositoryUploadStatusReady { + return errors.New("remote upload is not ready for failure import") + } + upload.Status = request.RepositoryUploadStatusFailed + upload.ErrorCode = errorCode + upload.ErrorMessage = errorMessage + return s.content.UpdateUpload(upload, session) +} + +// MarkUploadFailedFromTask moves the upload bound to a failed or canceled UPLOAD task out of a +// non-terminal state so it no longer blocks repository deletion. Repeated calls are idempotent. +func (s *service) MarkUploadFailedFromTask(task *po.RepositoryTask, errorCode, errorMessage string, + session entity.Session, +) error { + if task == nil || session == nil || task.Type != request.RepositoryTaskTypeUpload { + return nil + } + var snapshot request.RepositoryUploadSnapshot + if err := json.Unmarshal([]byte(task.PayloadJSON), &snapshot); err != nil || + uuid.Validate(snapshot.UploadID) != nil { + return nil + } + upload, err := s.content.GetUpload(snapshot.UploadID, true, session) + if err != nil { + return nil + } + if upload.RepositoryID != task.RepositoryID || !uploadTaskMatches(upload.TaskID, task) { + return nil + } + switch upload.Status { + case request.RepositoryUploadStatusReceiving, request.RepositoryUploadStatusReady, + request.RepositoryUploadStatusProcessing: + default: + return nil + } + if strings.TrimSpace(errorCode) == "" { + errorCode = "TASK_FAILED" + } + upload.Status = request.RepositoryUploadStatusFailed + upload.ErrorCode = limitText(errorCode, 64) + upload.ErrorMessage = limitText(errorMessage, 2048) + return s.content.UpdateUpload(upload, session) +} + +func limitText(value string, maximum int) string { + if utf8.RuneCountInString(value) <= maximum { + return value + } + return string([]rune(value)[:maximum]) +} + +// A retry gets a new task ID but intentionally reuses the immutable upload snapshot +// created by the root task. All other upload identity fields are still checked above. +func uploadTaskMatches(uploadTaskID string, task *po.RepositoryTask) bool { + if task == nil || uploadTaskID == "" { + return false + } + return uploadTaskID == task.ID || (task.RootTaskID != "" && uploadTaskID == task.RootTaskID) +} + +func sameRemoteRevision(left, right *po.RepositoryRevision) bool { + return left != nil && right != nil && left.ID == right.ID && left.RepositoryID == right.RepositoryID && + left.StorageNodeID == right.StorageNodeID && left.ParentRevisionID == right.ParentRevisionID && + left.TaskID == right.TaskID && left.Kind == right.Kind && left.Status == right.Status && + left.PackageCount == right.PackageCount && left.TotalBytes == right.TotalBytes && + left.ManifestSHA256 == right.ManifestSHA256 && left.RepomdSHA256 == right.RepomdSHA256 && + left.SnapshotManifestSHA256 == right.SnapshotManifestSHA256 && left.RelativePath == right.RelativePath +} + +func sameRemotePackages(left, right []po.RepositoryPackage) bool { + if len(left) != len(right) { + return false + } + byNEVRA := make(map[string]po.RepositoryPackage, len(left)) + for _, item := range left { + byNEVRA[item.NEVRA] = item + } + for _, item := range right { + stored, exists := byNEVRA[item.NEVRA] + if !exists || stored.RepositoryID != item.RepositoryID || stored.RevisionID != item.RevisionID || + stored.Name != item.Name || stored.Epoch != item.Epoch || stored.Version != item.Version || + stored.Release != item.Release || stored.Architecture != item.Architecture || + stored.FileName != item.FileName || stored.RelativePath != item.RelativePath || + stored.SizeBytes != item.SizeBytes || stored.SHA256 != item.SHA256 || stored.Source != item.Source || + stored.SourceUploadID != item.SourceUploadID { + return false + } + } + return true +} + +func sameRemoteQuarantine(left, right []po.RepositoryQuarantineItem) bool { + if len(left) != len(right) { + return false + } + byPath := make(map[string]po.RepositoryQuarantineItem, len(left)) + for _, item := range left { + byPath[item.RelativePath] = item + } + for _, item := range right { + stored, exists := byPath[item.RelativePath] + if !exists || stored.RepositoryID != item.RepositoryID || stored.TaskID != item.TaskID || + stored.UploadID != item.UploadID || stored.OriginalName != item.OriginalName || + stored.SizeBytes != item.SizeBytes || stored.SHA256 != item.SHA256 || + stored.ReasonCode != item.ReasonCode || stored.ReasonMessage != item.ReasonMessage || + stored.DetectedNEVRA != item.DetectedNEVRA { + return false + } + } + return true +} + +func (s *service) SyncRepository(ctx entity.Context, repositoryID string) (*po.RepositoryTask, error) { + return s.enqueueSync(ctx, repositoryID, request.RepositoryTaskTriggerManual, "") +} + +// SyncRepositoryScheduled reuses the product sync path while adding a durable +// schedule idempotency key. It is intentionally not exposed as a second API. +func (s *service) SyncRepositoryScheduled(ctx entity.Context, repositoryID, idempotencyKey string) ( + *po.RepositoryTask, error, +) { + return s.enqueueSync(ctx, repositoryID, request.RepositoryTaskTriggerScheduled, idempotencyKey) +} + +func (s *service) enqueueSync(ctx entity.Context, repositoryID, trigger, idempotencyKey string) ( + *po.RepositoryTask, error, +) { + repository, err := s.repositories.Describe(ctx, strings.TrimSpace(repositoryID)) + if err != nil { + return nil, err + } + if repository.SystemManaged || repository.Purpose == request.RepositoryPurposeNodeRPM { + return nil, ErrSystemRepositorySync + } + if !repository.Enabled || strings.TrimSpace(repository.UpstreamURL) == "" { + return nil, ErrSyncUnavailable + } + if idempotencyKey != "" { + if existing, lookupErr := s.tasks.GetByIdempotencyKey(idempotencyKey, nil); lookupErr == nil { + return existing, nil + } else if !errors.Is(lookupErr, repositorytaskrepo.ErrTaskNotFound) { + return nil, lookupErr + } + } + running, err := s.tasks.HasUnfinishedType(repository.ID, request.RepositoryTaskTypeSync, nil) + if err != nil { + return nil, err + } + if running { + return nil, ErrSyncAlreadyRunning + } + include, exclude, err := repository.PackageFilters() + if err != nil { + return nil, err + } + payload, err := json.Marshal(request.RepositorySyncSnapshot{ + UpstreamURL: repository.UpstreamURL, Architecture: repository.Architecture, + IncludePackages: include, ExcludePackages: exclude, SyncMode: repository.SyncMode, + BandwidthLimitBytes: repository.BandwidthLimitBytes, + MaxParallelDownloads: repository.MaxParallelDownloads, + }) + if err != nil { + return nil, err + } + return s.tasks.Enqueue(ctx, request.EnqueueRepositoryTaskRequest{ + RepositoryID: repository.ID, Type: request.RepositoryTaskTypeSync, + PayloadJSON: string(payload), MaxAttempts: 3, Trigger: trigger, + IdempotencyKey: idempotencyKey, BandwidthLimitBytes: repository.BandwidthLimitBytes, + MaxParallelDownloads: repository.MaxParallelDownloads, + }) +} + +func (s *service) BeginUpload(ctx entity.Context, input request.BeginRepositoryUploadRequest) ( + value *po.RepositoryUpload, err error, +) { + repository, err := s.repositories.Describe(ctx, strings.TrimSpace(input.RepositoryID)) + if err != nil { + return nil, err + } + if repository.Type != request.RepositoryTypePrivate || !repository.AllowUpload || !repository.Enabled { + return nil, ErrUploadDisabled + } + if ctx.User == nil || strings.TrimSpace(ctx.User.LoginName) == "" { + return nil, errors.New("authenticated user is required") + } + var kind string + input.FileName, kind, err = normalizeUploadFileName(input.FileName) + if err != nil { + return nil, err + } + cfg := s.store.Config() + if input.TotalSize <= 0 || input.TotalSize > cfg.MaxUploadBytes { + return nil, fmt.Errorf("total_size must be between 1 and %d bytes", cfg.MaxUploadBytes) + } + input.ExpectedSHA256 = strings.ToLower(strings.TrimSpace(input.ExpectedSHA256)) + if input.ExpectedSHA256 != "" && !sha256Pattern.MatchString(input.ExpectedSHA256) { + return nil, fmt.Errorf("expected_sha256 must be 64 lowercase hexadecimal characters") + } + input.ClientFingerprint = strings.ToLower(strings.TrimSpace(input.ClientFingerprint)) + if input.ClientFingerprint != "" && !sha256Pattern.MatchString(input.ClientFingerprint) { + return nil, fmt.Errorf("client_fingerprint must be 64 lowercase hexadecimal characters") + } + indexedUpload := input.ClientFingerprint != "" || input.ChunkSize > 0 + totalChunks := 0 + if indexedUpload { + if input.ChunkSize <= 0 { + input.ChunkSize = cfg.MaxChunkBytes + } + if input.ChunkSize > cfg.MaxChunkBytes { + return nil, fmt.Errorf("chunk_size must be between 1 and %d bytes", cfg.MaxChunkBytes) + } + totalChunks = int((input.TotalSize + int64(input.ChunkSize) - 1) / int64(input.ChunkSize)) + } + if input.ClientFingerprint != "" { + existing, findErr := s.content.FindResumableUpload(repository.ID, ctx.User.LoginName, + input.ClientFingerprint, nil) + if findErr != nil { + return nil, findErr + } + if existing != nil && existing.ExpiresAt.After(s.now()) && existing.FileName == input.FileName && + existing.ExpectedSize == input.TotalSize && existing.ChunkSize == input.ChunkSize && + existing.TotalChunks == totalChunks { + return existing, nil + } + } + id := uuid.NewString() + var relativePath string + var node *po.RepositoryStorageNode + if s.remoteUpload != nil { + node, err = s.remoteNode(repository.PrimaryStorageNodeID) + if err != nil { + return nil, err + } + relativePath, err = s.remoteUpload.BeginUpload(ctx.Context, node, id, input.TotalSize) + } else { + relativePath, err = s.store.CreateUpload(id, input.TotalSize) + } + if err != nil { + return nil, err + } + value = &po.RepositoryUpload{ + ID: id, RepositoryID: repository.ID, ProjectID: repository.ProjectID, + StorageNodeID: repository.PrimaryStorageNodeID, + ClientFingerprint: input.ClientFingerprint, + FileName: input.FileName, Kind: kind, ExpectedSize: input.TotalSize, + ChunkSize: input.ChunkSize, TotalChunks: totalChunks, + ExpectedSHA256: input.ExpectedSHA256, RelativePath: relativePath, + Status: request.RepositoryUploadStatusReceiving, RequestedBy: ctx.User.LoginName, + ExpiresAt: s.now().Add(time.Duration(cfg.UploadTTLHours) * time.Hour), + } + if err = s.content.CreateUpload(value, nil); err != nil { + if s.remoteUpload != nil { + _ = s.remoteUpload.AbortUpload(ctx.Context, node, id) + } else { + _, _ = s.store.MoveToTrash(relativePath, "failed-uploads", id) + } + return nil, err + } + return value, nil +} + +func (s *service) AppendChunk(ctx entity.Context, input request.AppendRepositoryUploadChunkRequest) ( + value *po.RepositoryUpload, err error, +) { + if err = validateUUID("upload_id", input.UploadID); err != nil { + return nil, err + } + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + value, err = s.content.GetUpload(input.UploadID, true, tx) + if err != nil { + return nil, err + } + if err = s.authorizeUpload(ctx, value); err != nil { + return nil, err + } + if value.Status != request.RepositoryUploadStatusReceiving { + return nil, ErrUploadNotReceiving + } + if value.TotalChunks > 0 { + return nil, errors.New("indexed upload session requires PutRepositoryUploadChunk") + } + if !value.ExpiresAt.After(s.now()) { + return nil, ErrUploadExpired + } + if input.Offset != value.ReceivedSize { + return nil, fmt.Errorf("%w: expected=%d actual=%d", storage.ErrOffsetMismatch, + value.ReceivedSize, input.Offset) + } + var next int64 + if s.remoteUpload != nil { + node, nodeErr := s.remoteNode(value.StorageNodeID) + if nodeErr != nil { + return nil, nodeErr + } + next, err = s.remoteUpload.AppendUploadChunk(ctx.Context, node, value.ID, + value.ExpectedSize, input.Offset, input.Data) + } else { + next, err = s.store.Append(value.RelativePath, input.Offset, input.Data, value.ExpectedSize) + } + if err != nil { + return nil, err + } + value.ReceivedSize = next + if err = s.content.UpdateUpload(value, tx); err != nil { + return nil, err + } + return value, nil +} + +func (s *service) PutChunk(ctx entity.Context, input request.PutRepositoryUploadChunkRequest) ( + value *po.RepositoryUpload, stored *po.RepositoryUploadChunk, err error, +) { + if err = validateUUID("upload_id", input.UploadID); err != nil { + return nil, nil, err + } + input.ChunkSHA256 = strings.ToLower(strings.TrimSpace(input.ChunkSHA256)) + if input.ChunkIndex < 0 || !sha256Pattern.MatchString(input.ChunkSHA256) || len(input.Data) == 0 { + return nil, nil, errors.New("upload chunk metadata is invalid") + } + digest := sha256.Sum256(input.Data) + if hex.EncodeToString(digest[:]) != input.ChunkSHA256 { + return nil, nil, errors.New("upload chunk checksum mismatch") + } + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + value, err = s.content.GetUpload(input.UploadID, true, tx) + if err != nil { + return nil, nil, err + } + if err = s.authorizeUpload(ctx, value); err != nil { + return nil, nil, err + } + if value.Status != request.RepositoryUploadStatusReceiving { + return nil, nil, ErrUploadNotReceiving + } + if !value.ExpiresAt.After(s.now()) { + return nil, nil, ErrUploadExpired + } + if value.TotalChunks <= 0 || input.ChunkIndex >= value.TotalChunks || len(input.Data) > value.ChunkSize { + return nil, nil, errors.New("upload chunk index or size is outside the declared session") + } + expectedSize := value.ChunkSize + if input.ChunkIndex == value.TotalChunks-1 { + expectedSize = int(value.ExpectedSize - int64(input.ChunkIndex*value.ChunkSize)) + } + if len(input.Data) != expectedSize { + return nil, nil, fmt.Errorf("upload chunk size mismatch: expected=%d actual=%d", expectedSize, len(input.Data)) + } + existingChunk, err := s.content.GetUploadChunk(value.ID, input.ChunkIndex, tx) + if err != nil { + return nil, nil, err + } + if existingChunk != nil && (existingChunk.SHA256 != input.ChunkSHA256 || + existingChunk.SizeBytes != len(input.Data)) { + return nil, nil, ErrUploadChunkConflict + } + var storedPath string + if s.remoteUpload != nil { + node, nodeErr := s.remoteNode(value.StorageNodeID) + if nodeErr != nil { + return nil, nil, nodeErr + } + storedPath, err = s.remoteUpload.PutUploadChunk(ctx.Context, node, value.ID, + input.ChunkIndex, input.Data, input.ChunkSHA256) + } else { + storedPath, err = s.store.PutUploadChunk(value.ID, input.ChunkIndex, input.Data, input.ChunkSHA256) + } + if err != nil { + return nil, nil, err + } + chunk := &po.RepositoryUploadChunk{UploadID: value.ID, ChunkIndex: input.ChunkIndex, + SizeBytes: len(input.Data), SHA256: input.ChunkSHA256, StoredPath: storedPath} + stored, _, err = s.content.PutUploadChunk(chunk, tx) + if err != nil { + return nil, nil, err + } + if stored.SHA256 != chunk.SHA256 || stored.SizeBytes != chunk.SizeBytes || stored.StoredPath != chunk.StoredPath { + return nil, nil, ErrUploadChunkConflict + } + chunks, err := s.content.ListUploadChunks(value.ID, tx) + if err != nil { + return nil, nil, err + } + value.UploadedCount, value.ReceivedSize = len(chunks), 0 + for index := range chunks { + value.ReceivedSize += int64(chunks[index].SizeBytes) + } + if err = s.content.UpdateUpload(value, tx); err != nil { + return nil, nil, err + } + return value, stored, nil +} + +func (s *service) DescribeUploadChunks(ctx entity.Context, uploadID string) ( + *po.RepositoryUpload, []po.RepositoryUploadChunk, []int, error, +) { + value, err := s.DescribeUpload(ctx, uploadID) + if err != nil { + return nil, nil, nil, err + } + chunks, err := s.content.ListUploadChunks(uploadID, nil) + if err != nil { + return nil, nil, nil, err + } + present := make(map[int]struct{}, len(chunks)) + for index := range chunks { + present[chunks[index].ChunkIndex] = struct{}{} + } + missingCapacity := value.TotalChunks - len(chunks) + if missingCapacity < 0 { + missingCapacity = 0 + } + missing := make([]int, 0, missingCapacity) + for index := 0; index < value.TotalChunks; index++ { + if _, ok := present[index]; !ok { + missing = append(missing, index) + } + } + return value, chunks, missing, nil +} + +func (s *service) PauseUpload(ctx entity.Context, uploadID string) (value *po.RepositoryUpload, err error) { + return s.changeUploadPauseState(ctx, uploadID, true) +} + +func (s *service) ResumeUpload(ctx entity.Context, uploadID string) (value *po.RepositoryUpload, err error) { + return s.changeUploadPauseState(ctx, uploadID, false) +} + +func (s *service) changeUploadPauseState(ctx entity.Context, uploadID string, pause bool) ( + value *po.RepositoryUpload, err error, +) { + if err = validateUUID("upload_id", uploadID); err != nil { + return nil, err + } + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + value, err = s.content.GetUpload(uploadID, true, tx) + if err != nil { + return nil, err + } + if err = s.authorizeUpload(ctx, value); err != nil { + return nil, err + } + now := s.now() + if pause { + if value.Status == request.RepositoryUploadStatusPaused { + return value, nil + } + if value.Status != request.RepositoryUploadStatusReceiving { + return nil, ErrUploadNotReceiving + } + value.Status, value.PausedAt = request.RepositoryUploadStatusPaused, &now + } else { + if value.Status == request.RepositoryUploadStatusReceiving { + return value, nil + } + if value.Status != request.RepositoryUploadStatusPaused || !value.ExpiresAt.After(now) { + return nil, ErrUploadNotReceiving + } + value.Status, value.PausedAt = request.RepositoryUploadStatusReceiving, nil + } + err = s.content.UpdateUpload(value, tx) + return value, err +} + +func (s *service) CompleteUpload(ctx entity.Context, uploadID string) ( + value *po.RepositoryUpload, task *po.RepositoryTask, err error, +) { + if err = validateUUID("upload_id", uploadID); err != nil { + return nil, nil, err + } + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + value, err = s.content.GetUpload(uploadID, true, tx) + if err != nil { + return nil, nil, err + } + if err = s.authorizeUpload(ctx, value); err != nil { + return nil, nil, err + } + if value.TaskID != "" { + task, err = s.tasks.Describe(ctx, value.TaskID) + return value, task, err + } + if value.Status != request.RepositoryUploadStatusReceiving { + return nil, nil, ErrUploadNotReceiving + } + if value.TotalChunks > 0 { + chunks, listErr := s.content.ListUploadChunks(value.ID, tx) + if listErr != nil { + return nil, nil, listErr + } + var receivedSize int64 + for index := range chunks { + receivedSize += int64(chunks[index].SizeBytes) + } + if len(chunks) != value.TotalChunks || receivedSize != value.ExpectedSize { + return nil, nil, fmt.Errorf("%w: expected_chunks=%d uploaded_chunks=%d received=%d", + ErrUploadIncomplete, value.TotalChunks, len(chunks), receivedSize) + } + for index := range chunks { + if chunks[index].ChunkIndex != index { + return nil, nil, fmt.Errorf("%w: missing chunk %d", ErrUploadIncomplete, index) + } + } + value.UploadedCount, value.ReceivedSize = len(chunks), receivedSize + } + digest, size, err := s.uploadDigestFor(ctx, value) + if err != nil { + return nil, nil, err + } + if size != value.ExpectedSize || value.ReceivedSize != value.ExpectedSize { + return nil, nil, fmt.Errorf("%w: expected=%d received=%d stored=%d", ErrUploadIncomplete, + value.ExpectedSize, value.ReceivedSize, size) + } + if value.ExpectedSHA256 != "" && digest != value.ExpectedSHA256 { + trashRelative := "" + if s.remoteUpload != nil { + node, nodeErr := s.remoteNode(value.StorageNodeID) + if nodeErr != nil { + return nil, nil, nodeErr + } + if moveErr := s.remoteUpload.AbortUpload(ctx.Context, node, value.ID); moveErr != nil { + return nil, nil, moveErr + } + trashRelative = filepath.ToSlash(filepath.Join("trash", "aborted-uploads", value.ID)) + } else { + var moveErr error + trashRelative, moveErr = s.store.MoveToTrash(value.RelativePath, "checksum-failed-uploads", value.ID) + if moveErr != nil { + return nil, nil, moveErr + } + } + value.Status = request.RepositoryUploadStatusFailed + value.ErrorCode = "UPLOAD_CHECKSUM_MISMATCH" + value.ErrorMessage = ErrUploadChecksum.Error() + value.ActualSHA256 = digest + value.RelativePath = filepath.ToSlash(filepath.Join(trashRelative, "content")) + if updateErr := s.content.UpdateUpload(value, tx); updateErr != nil { + return nil, nil, updateErr + } + return value, nil, nil + } + payload, _ := json.Marshal(request.RepositoryUploadSnapshot{UploadID: value.ID, + FileName: value.FileName, Kind: value.Kind, RelativePath: value.RelativePath, + SizeBytes: size, SHA256: digest}) + task, err = s.tasks.EnqueueInSession(ctx, request.EnqueueRepositoryTaskRequest{ + RepositoryID: value.RepositoryID, Type: request.RepositoryTaskTypeUpload, + PayloadJSON: string(payload), MaxAttempts: 3, + }, tx) + if err != nil { + return nil, nil, err + } + now := s.now() + value.TaskID = task.ID + value.Status = request.RepositoryUploadStatusReady + value.ActualSHA256 = digest + value.CompletedAt = &now + if err = s.content.UpdateUpload(value, tx); err != nil { + return nil, nil, err + } + return value, task, nil +} + +func (s *service) AbortUpload(ctx entity.Context, uploadID string) (value *po.RepositoryUpload, err error) { + if err = validateUUID("upload_id", uploadID); err != nil { + return nil, err + } + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + value, err = s.content.GetUpload(uploadID, true, tx) + if err != nil { + return nil, err + } + if err = s.authorizeUpload(ctx, value); err != nil { + return nil, err + } + if value.Status != request.RepositoryUploadStatusReceiving && value.Status != request.RepositoryUploadStatusPaused { + return nil, ErrUploadNotReceiving + } + value.Status = request.RepositoryUploadStatusAborted + if err = s.content.UpdateUpload(value, tx); err != nil { + return nil, err + } + var moveErr error + if s.remoteUpload != nil { + node, nodeErr := s.remoteNode(value.StorageNodeID) + if nodeErr != nil { + return value, nodeErr + } + moveErr = s.remoteUpload.AbortUpload(ctx.Context, node, value.ID) + } else { + _, moveErr = s.store.MoveToTrash(value.RelativePath, "aborted-uploads", value.ID) + } + if moveErr != nil { + value.ErrorCode = "UPLOAD_TRASH_MOVE_FAILED" + value.ErrorMessage = moveErr.Error() + _ = s.content.UpdateUpload(value, tx) + return value, moveErr + } + return value, nil +} + +func (s *service) DescribeUpload(ctx entity.Context, uploadID string) (*po.RepositoryUpload, error) { + if err := validateUUID("upload_id", uploadID); err != nil { + return nil, err + } + value, err := s.content.GetUpload(uploadID, false, nil) + if err != nil { + return nil, err + } + if err = s.authorizeUpload(ctx, value); err != nil { + return nil, err + } + return value, nil +} + +func (s *service) ListPackages(ctx entity.Context, filter request.ListRepositoryPackagesRequest) ( + []po.RepositoryPackage, string, request.PageResponse, error, +) { + repository, err := s.repositories.Describe(ctx, strings.TrimSpace(filter.RepositoryID)) + if err != nil { + return nil, "", request.PageResponse{}, err + } + filter.RepositoryID = repository.ID + if filter.RevisionID == "" { + if repository.ActiveRevisionID != "" { + filter.RevisionID = repository.ActiveRevisionID + } + } + if filter.RevisionID == "" { + revision, findErr := s.content.LatestReadyRevision(repository.ID, nil) + if findErr != nil { + return nil, "", request.PageResponse{}, findErr + } + if revision == nil { + return []po.RepositoryPackage{}, "", request.PageResponse{}, nil + } + filter.RevisionID = revision.ID + } + if filter.RevisionID != "" { + if _, err = s.content.GetRevision(repository.ID, filter.RevisionID, nil); err != nil { + return nil, "", request.PageResponse{}, err + } + } else { + return []po.RepositoryPackage{}, "", request.PageResponse{}, nil + } + filter.Name = strings.TrimSpace(filter.Name) + filter.Architecture = strings.ToLower(strings.TrimSpace(filter.Architecture)) + filter.PageRequest = normalizePage(filter.PageRequest) + values, page, err := s.content.ListPackages(filter, nil) + return values, filter.RevisionID, page, err +} + +func (s *service) ListQuarantine(ctx entity.Context, filter request.ListRepositoryQuarantineRequest) ( + []po.RepositoryQuarantineItem, request.PageResponse, error, +) { + repository, err := s.repositories.Describe(ctx, strings.TrimSpace(filter.RepositoryID)) + if err != nil { + return nil, request.PageResponse{}, err + } + filter.RepositoryID = repository.ID + filter.TaskID = strings.TrimSpace(filter.TaskID) + if filter.TaskID != "" { + if err = validateUUID("task_id", filter.TaskID); err != nil { + return nil, request.PageResponse{}, err + } + } + filter.ReasonCode = strings.ToUpper(strings.TrimSpace(filter.ReasonCode)) + filter.PageRequest = normalizePage(filter.PageRequest) + return s.content.ListQuarantine(filter, nil) +} + +func (s *service) DeletePackage(ctx entity.Context, input request.DeleteRepositoryPackageRequest) ( + *po.RepositoryTask, error, +) { + repository, err := s.repositories.Describe(ctx, strings.TrimSpace(input.RepositoryID)) + if err != nil { + return nil, err + } + nevra := strings.TrimSpace(input.NEVRA) + if nevra == "" || utf8.RuneCountInString(nevra) > 512 || strings.IndexFunc(nevra, unicode.IsControl) >= 0 { + return nil, fmt.Errorf("nevra is invalid or exceeds 512 characters") + } + payload, _ := json.Marshal(map[string]string{"nevra": nevra}) + return s.tasks.Enqueue(ctx, request.EnqueueRepositoryTaskRequest{ + RepositoryID: repository.ID, Type: request.RepositoryTaskTypeDeletePackage, + PayloadJSON: string(payload), MaxAttempts: 3, + }) +} + +func (s *service) uploadDigestFor(ctx entity.Context, value *po.RepositoryUpload) (string, int64, error) { + if s.remoteUpload != nil { + node, nodeErr := s.remoteNode(value.StorageNodeID) + if nodeErr != nil { + return "", 0, nodeErr + } + _, size, digest, completeErr := s.remoteUpload.CompleteUpload(ctx.Context, node, value.ID, + value.ExpectedSize, "", value.TotalChunks) + return digest, size, completeErr + } + if value.TotalChunks > 0 { + _, size, digest, assembleErr := s.store.AssembleUpload(value.ID, value.TotalChunks, value.ExpectedSize) + return digest, size, assembleErr + } + return s.store.SHA256(value.RelativePath) +} + +func (s *service) remoteNode(storageNodeID string) (*po.RepositoryStorageNode, error) { + if s.nodes == nil || storageNodeID == "" { + return nil, errors.New("repository storage node is not configured") + } + node, err := s.nodes.DescribeInternal(storageNodeID) + if err != nil { + return nil, err + } + if !node.Enabled || node.Status != request.RepositoryStorageNodeStatusOnline { + return nil, errors.New("repository storage node is not online") + } + return node, nil +} + +func (s *service) authorizeUpload(ctx entity.Context, upload *po.RepositoryUpload) error { + repository, err := s.repositories.Describe(ctx, upload.RepositoryID) + if err != nil { + return err + } + if repository.ProjectID != upload.ProjectID { + return errors.New("repository upload project mismatch") + } + if ctx.User == nil || strings.TrimSpace(ctx.User.LoginName) == "" { + return errors.New("authenticated user is required") + } + if upload.RequestedBy != ctx.User.LoginName && !internal.IsGlobalAdministrator(ctx) { + return ErrUploadOwnerMismatch + } + return nil +} + +func normalizeUploadFileName(value string) (string, string, error) { + value = filepath.Base(strings.TrimSpace(value)) + if value == "." || value == "" || utf8.RuneCountInString(value) > 255 || + strings.IndexFunc(value, unicode.IsControl) >= 0 { + return "", "", fmt.Errorf("file_name is invalid or exceeds 255 characters") + } + lower := strings.ToLower(value) + switch { + case strings.HasSuffix(lower, ".rpm"): + return value, request.RepositoryUploadKindRPM, nil + case strings.HasSuffix(lower, ".tar.gz"): + return value, request.RepositoryUploadKindTarGZ, nil + default: + return "", "", fmt.Errorf("only .rpm and .tar.gz uploads are supported") + } +} + +func validateUUID(field, value string) error { + if _, err := uuid.Parse(strings.TrimSpace(value)); err != nil { + return fmt.Errorf("invalid %s: %w", field, err) + } + return nil +} + +func normalizePage(value request.PageRequest) request.PageRequest { + if value.PageNumber <= 0 { + value.PageNumber = 1 + } + if value.PageSize <= 0 { + value.PageSize = 50 + } + if value.PageSize > 200 { + value.PageSize = 200 + } + return value +} + +func SHA256Bytes(value []byte) string { + digest := sha256.Sum256(value) + return hex.EncodeToString(digest[:]) +} diff --git a/manager/backend/services/footstone/domain/repositoryContentService/system_repository_test.go b/manager/backend/services/footstone/domain/repositoryContentService/system_repository_test.go new file mode 100644 index 0000000000000000000000000000000000000000..4f5787465751d9ab783dd9056bd64cb9e16c8614 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryContentService/system_repository_test.go @@ -0,0 +1,31 @@ +package repositoryContentService + +import ( + "errors" + "testing" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +type systemRepositoryManager struct { + repository *po.SoftwareRepository +} + +func (m systemRepositoryManager) Describe(entity.Context, string) (*po.SoftwareRepository, error) { + return m.repository, nil +} + +func TestSyncRepositoryRejectsSystemManagedNodeRPMRepository(t *testing.T) { + service := &service{repositories: systemRepositoryManager{repository: &po.SoftwareRepository{ + ID: "11111111-1111-4111-8111-111111111111", ProjectID: 42, Enabled: true, + SystemManaged: true, Purpose: request.RepositoryPurposeNodeRPM, + UpstreamURL: "https://example.invalid/repository/", + }}} + + _, err := service.SyncRepository(entity.Context{}, "11111111-1111-4111-8111-111111111111") + if !errors.Is(err, ErrSystemRepositorySync) { + t.Fatalf("SyncRepository() error = %v, want ErrSystemRepositorySync", err) + } +} diff --git a/manager/backend/services/footstone/domain/repositoryContentService/upload_session_test.go b/manager/backend/services/footstone/domain/repositoryContentService/upload_session_test.go new file mode 100644 index 0000000000000000000000000000000000000000..a4a3edfc2f0227b95551b5029c414ca2cd3047e8 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryContentService/upload_session_test.go @@ -0,0 +1,146 @@ +package repositoryContentService + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "strings" + "testing" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + ftpb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/footstone" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorycontentrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" +) + +type uploadSessionMemoryRepository struct { + repositorycontentrepo.Repository + uploads []*po.RepositoryUpload +} + +func (r *uploadSessionMemoryRepository) CreateUpload(value *po.RepositoryUpload, _ entity.Session) error { + r.uploads = append(r.uploads, value) + return nil +} + +func (r *uploadSessionMemoryRepository) FindResumableUpload(repositoryID, requestedBy, fingerprint string, + _ entity.Session, +) (*po.RepositoryUpload, error) { + for index := len(r.uploads) - 1; index >= 0; index-- { + value := r.uploads[index] + if value.RepositoryID == repositoryID && value.RequestedBy == requestedBy && + value.ClientFingerprint == fingerprint && + (value.Status == request.RepositoryUploadStatusReceiving || value.Status == request.RepositoryUploadStatusPaused) { + return value, nil + } + } + return nil, nil +} + +func TestBeginUploadSameNameAfterFailureUsesIndependentSession(t *testing.T) { + for _, indexed := range []bool{false, true} { + name := "legacy" + if indexed { + name = "indexed" + } + t.Run(name, func(t *testing.T) { + service, records, ctx := newUploadSessionTestService(t) + input := request.BeginRepositoryUploadRequest{ + RepositoryID: "11111111-1111-4111-8111-111111111111", FileName: "same-name.rpm", TotalSize: 6, + } + if indexed { + input.ClientFingerprint, input.ChunkSize = strings.Repeat("a", 64), 4 + } + failed, err := service.BeginUpload(ctx, input) + if err != nil { + t.Fatal(err) + } + if _, err = service.store.Append(failed.RelativePath, 0, []byte("bro"), 6); err != nil { + t.Fatal(err) + } + if _, err = service.store.Append(failed.RelativePath, 3, []byte("ken"), 6); err != nil { + t.Fatal(err) + } + failed.Status = request.RepositoryUploadStatusFailed + if _, err = service.store.MoveToTrash(failed.RelativePath, "processed-uploads", failed.ID); err != nil { + t.Fatal(err) + } + replacement, err := service.BeginUpload(ctx, input) + if err != nil { + t.Fatal(err) + } + if replacement.ID == failed.ID || replacement.RelativePath == failed.RelativePath || + replacement.FileName != failed.FileName || len(records.uploads) != 2 { + t.Fatalf("same-name upload reused a failed session: failed=%#v replacement=%#v", failed, replacement) + } + if _, err = service.store.Append(replacement.RelativePath, 0, []byte("new"), 6); err != nil { + t.Fatal(err) + } + if _, err = service.store.Append(replacement.RelativePath, 3, []byte("rpm"), 6); err != nil { + t.Fatal(err) + } + digest, size, err := service.store.SHA256(replacement.RelativePath) + want := sha256.Sum256([]byte("newrpm")) + if err != nil || size != 6 || digest != hex.EncodeToString(want[:]) { + t.Fatalf("replacement upload content = %s/%d/%v", digest, size, err) + } + }) + } +} + +func TestBeginUploadResumesWithoutRecreatingStoredChunks(t *testing.T) { + for _, status := range []string{request.RepositoryUploadStatusReceiving, request.RepositoryUploadStatusPaused} { + t.Run(status, func(t *testing.T) { + service, records, ctx := newUploadSessionTestService(t) + input := request.BeginRepositoryUploadRequest{ + RepositoryID: "11111111-1111-4111-8111-111111111111", FileName: "same-name.rpm", + TotalSize: 6, ChunkSize: 4, ClientFingerprint: strings.Repeat("a", 64), + } + original, err := service.BeginUpload(ctx, input) + if err != nil { + t.Fatal(err) + } + original.Status = status + chunk := []byte("abcd") + chunkDigest := sha256.Sum256(chunk) + chunkSHA := hex.EncodeToString(chunkDigest[:]) + chunkPath, err := service.store.PutUploadChunk(original.ID, 0, chunk, chunkSHA) + if err != nil { + t.Fatal(err) + } + resumed, err := service.BeginUpload(ctx, input) + if err != nil { + t.Fatal(err) + } + if resumed.ID != original.ID || len(records.uploads) != 1 { + t.Fatalf("resume created another upload: original=%s resumed=%s", original.ID, resumed.ID) + } + actualSHA, size, err := service.store.SHA256(chunkPath) + if err != nil || actualSHA != chunkSHA || size != 4 { + t.Fatalf("resume changed an existing chunk: %s/%d/%v", actualSHA, size, err) + } + }) + } +} + +func newUploadSessionTestService(t *testing.T) (*service, *uploadSessionMemoryRepository, entity.Context) { + t.Helper() + store, err := storage.New(config.RepositoryConfig{Root: t.TempDir(), MinimumFreeBytes: 1, + MaxUploadBytes: 64, MaxChunkBytes: 4}) + if err != nil { + t.Fatal(err) + } + clock := time.Date(2026, 10, 23, 0, 0, 0, 0, time.UTC) + records := &uploadSessionMemoryRepository{} + value := &service{store: store, content: records, now: func() time.Time { return clock }, + repositories: systemRepositoryManager{repository: &po.SoftwareRepository{ + ID: "11111111-1111-4111-8111-111111111111", ProjectID: 42, Enabled: true, + Type: request.RepositoryTypePrivate, AllowUpload: true, + }}} + ctx := entity.Context{Context: context.Background(), User: &ftpb.UserInfo{LoginName: "review-user"}} + return value, records, ctx +} diff --git a/manager/backend/services/footstone/domain/repositoryDiagnosisService/service.go b/manager/backend/services/footstone/domain/repositoryDiagnosisService/service.go new file mode 100644 index 0000000000000000000000000000000000000000..7d917ae9cdd39c87a6afb7506fec9c0cc9531903 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryDiagnosisService/service.go @@ -0,0 +1,366 @@ +package repositoryDiagnosisService + +import ( + "encoding/json" + "fmt" + "regexp" + "strings" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryErrorCatalog" +) + +const ( + CategoryNetwork = "NETWORK" + CategoryUpstreamHTTP = "UPSTREAM_HTTP" + CategoryMetadata = "METADATA" + CategoryDownload = "DOWNLOAD" + CategoryChecksum = "CHECKSUM" + CategoryArchitecture = "ARCHITECTURE" + CategoryPackage = "PACKAGE" + CategoryDependency = "DEPENDENCY" + CategoryGPG = "GPG" + CategorySecurity = "SECURITY" + CategoryStorage = "STORAGE" + CategoryPublication = "PUBLICATION" + CategoryConfiguration = "CONFIGURATION" + CategoryTask = "TASK" +) + +var ( + credentialURLPattern = regexp.MustCompile(`(?i)([a-z][a-z0-9+.-]*://)([^/@\s:]+):([^/@\s]+)@`) + secretPattern = regexp.MustCompile(`(?i)(authorization|proxy-authorization|password|passwd|token|secret|api[_-]?key)(\s*[:=]\s*)([^\s,;]+)`) + bearerPattern = regexp.MustCompile(`(?i)\b(bearer|basic)\s+[a-z0-9._~+/=-]+`) + httpStatusPattern = regexp.MustCompile(`(?i)(?:http(?: error)?|status(?: code)?)[^0-9]{0,8}([45][0-9]{2})`) +) + +type Context struct { + TaskType string + Status string + ErrorCode string + ErrorMessage string + CurrentStep string + RevisionID string +} + +type ruleResult struct { + Category string + Step string + Cause string + Action string + RuleID string +} + +func Analyze(input Context, generatedAt time.Time) *po.RepositoryDiagnosis { + input.TaskType = strings.ToUpper(strings.TrimSpace(input.TaskType)) + input.Status = strings.ToUpper(strings.TrimSpace(input.Status)) + input.ErrorCode = strings.ToUpper(strings.TrimSpace(input.ErrorCode)) + input.ErrorMessage = Sanitize(input.ErrorMessage) + if input.Status == request.RepositoryTaskStatusSuccess { + return nil + } + if input.ErrorCode == "" { + input.ErrorCode = "TASK_FAILED" + } + + result := diagnose(input) + localized := repositoryErrorCatalog.Resolve(input.ErrorCode, result.RuleID) + if input.CurrentStep != "" && result.Step == "execute repository task" { + result.Step = truncate(Sanitize(input.CurrentStep), 128) + } + evidence := []string{ + "task_type=" + input.TaskType, + "status=" + input.Status, + "error_code=" + input.ErrorCode, + } + if input.CurrentStep != "" { + evidence = append(evidence, "current_step="+truncate(Sanitize(input.CurrentStep), 128)) + } + if input.ErrorMessage != "" { + evidence = append(evidence, "message="+truncate(input.ErrorMessage, 512)) + } + if input.RevisionID != "" { + evidence = append(evidence, "revision_id="+truncate(Sanitize(input.RevisionID), 64)) + } + return &po.RepositoryDiagnosis{ + FailureCategory: result.Category, + ErrorCode: input.ErrorCode, + FailedStep: result.Step, + LikelyCause: localized.ZhMessage, + Evidence: evidence, + RecommendedAction: localized.ZhRecommendedAction, + RuleID: result.RuleID, + GeneratedAt: generatedAt.UTC(), + } +} + +func Apply(task *po.RepositoryTask, diagnosis *po.RepositoryDiagnosis) error { + clear(task) + if diagnosis == nil { + return nil + } + if task.ErrorCode == "" { + task.ErrorCode = truncate(diagnosis.ErrorCode, 64) + } + evidence, err := json.Marshal(diagnosis.Evidence) + if err != nil { + return fmt.Errorf("encode repository diagnosis evidence: %w", err) + } + task.FailureCategory = truncate(diagnosis.FailureCategory, 64) + task.FailedStep = truncate(diagnosis.FailedStep, 128) + task.LikelyCause = truncate(diagnosis.LikelyCause, 2048) + task.DiagnosisEvidenceJSON = evidence + task.RecommendedAction = truncate(diagnosis.RecommendedAction, 2048) + task.DiagnosisRuleID = truncate(diagnosis.RuleID, 128) + generatedAt := diagnosis.GeneratedAt.UTC() + task.DiagnosisGeneratedAt = &generatedAt + return nil +} + +func clear(task *po.RepositoryTask) { + if task == nil { + return + } + task.FailureCategory = "" + task.FailedStep = "" + task.LikelyCause = "" + task.DiagnosisEvidenceJSON = nil + task.RecommendedAction = "" + task.DiagnosisRuleID = "" + task.DiagnosisGeneratedAt = nil +} + +func diagnose(input Context) ruleResult { + message := strings.ToLower(input.ErrorMessage) + if value, ok := diagnoseNodePreflight(input.ErrorCode); ok { + return value + } + if input.TaskType == request.RepositoryTaskTypeValidate { + return diagnoseValidation(input.ErrorCode, message) + } + if input.TaskType == request.RepositoryTaskTypeSync { + return diagnoseSync(input.ErrorCode, message) + } + if input.TaskType == request.RepositoryTaskTypeUpload { + return diagnoseUpload(input.ErrorCode, message) + } + return diagnoseCommon(input.ErrorCode, message) +} + +func diagnoseNodePreflight(code string) (ruleResult, bool) { + switch code { + case "REPOSITORY_NODE_OFFLINE": + return result(CategoryNetwork, "contact repository storage node", + "The assigned repository storage node is offline or its heartbeat has expired.", + "Restore the node and its worker heartbeat, run Probe again, then retry the mutation.", + "NODE_OFFLINE"), true + case "REPOSITORY_NODE_UNAUTHORIZED": + return result(CategorySecurity, "authenticate repository storage node", + "The management plane and repository node could not establish an authorized mTLS session.", + "Verify the node certificate chain, server name and certificate fingerprint before probing again.", + "NODE_MTLS_UNAUTHORIZED"), true + case "REPOSITORY_NODE_PRECHECK_STALE": + return result(CategoryConfiguration, "revalidate repository node preflight", + "The task preflight record is missing, expired or no longer matches the assigned node.", + "Run a fresh node preflight and retry the task with the new preflight record.", + "NODE_PREFLIGHT_STALE"), true + case "REPOSITORY_NODE_INSUFFICIENT_SPACE": + return result(CategoryStorage, "preflight repository capacity", + "The repository node does not have enough free space for staging, the new immutable revision and the safety margin.", + "Review the recorded free_bytes and required_free_bytes, clean eligible staging/quarantine/trash data, expand storage or select another node, then run preflight again.", + "NODE_INSUFFICIENT_SPACE"), true + case "REPOSITORY_NODE_INODE_EXHAUSTED": + return result(CategoryStorage, "preflight repository inode capacity", + "The repository node does not have enough free inodes for the estimated package and metadata files.", + "Review free_inodes and required_inodes, remove eligible data or select a filesystem with sufficient inodes, then run preflight again.", + "NODE_INODE_EXHAUSTED"), true + case "REPOSITORY_NODE_TOOL_MISSING": + return result(CategoryConfiguration, "verify repository node tools", + "The repository node is missing a required RPM, metadata, GPG or transfer tool.", + "Install the tool reported by Probe on the repository node, verify its version, then rerun preflight.", + "NODE_TOOL_MISSING"), true + case "REPOSITORY_NODE_FS_UNSUPPORTED": + return result(CategoryStorage, "verify repository filesystem capabilities", + "The repository data filesystem does not satisfy atomic rename or relative symlink publication requirements.", + "Move repository-data to a supported bind-mounted filesystem, verify rename and symlink operations, then rerun Probe.", + "NODE_FILESYSTEM_UNSUPPORTED"), true + default: + return ruleResult{}, false + } +} + +func diagnoseSync(code, message string) ruleResult { + if code == "SYNC_GPG_VERIFICATION_FAILED" || hasAny(message, "unsigned_rpm", "unauthorized_gpg_key", "bad signature") { + return result(CategoryGPG, "verify synchronized RPM signatures", "At least one synchronized RPM is unsigned or not signed by an authorized repository key.", + "Keep the snapshot unpublished, correct the upstream signing policy or register the intended public key, then retry the full sync.", "SYNC_GPG_FAILURE") + } + if hasAny(message, "could not resolve host", "temporary failure in name resolution", "name or service not known", "no such host") { + return result(CategoryNetwork, "resolve upstream host", "The upstream host could not be resolved by DNS.", + "Verify DNS servers, the upstream hostname, and network policy from the repository worker.", "SYNC_DNS_FAILURE") + } + if status := httpStatus(message); status != "" { + category, rule := CategoryUpstreamHTTP, "SYNC_HTTP_"+status[:1]+"XX" + return result(category, "request upstream repository", "The upstream repository returned HTTP "+status+".", + "Verify the upstream path, credentials and server health, then retry the immutable sync snapshot.", rule) + } + if hasAny(message, "connection refused", "connection timed out", "network is unreachable", "couldn't connect", "could not connect", "timeout was reached") { + return result(CategoryNetwork, "connect to upstream", "The repository worker could not connect to the upstream endpoint.", + "Check routing, firewall rules, proxy settings and upstream availability from the worker container.", "SYNC_UPSTREAM_UNREACHABLE") + } + if hasAny(message, "checksum", "digest mismatch", "hash mismatch") { + return result(CategoryChecksum, "verify downloaded content", "Downloaded metadata or RPM content did not match its declared checksum.", + "Do not publish the candidate revision; refresh upstream metadata and repeat the full snapshot sync.", "SYNC_CHECKSUM_FAILURE") + } + if hasAny(message, "repomd.xml", "primary.xml", "metadata", "xml parse", "sqlite") { + return result(CategoryMetadata, "validate upstream metadata", "The upstream repository metadata is missing or invalid.", + "Run repository metadata validation against the upstream and rebuild or replace its repodata before retrying.", "SYNC_METADATA_INVALID") + } + if hasAny(message, "architecture", "wrong arch", "arch mismatch") { + return result(CategoryArchitecture, "filter package architecture", "The synchronized content does not match the repository architecture.", + "Verify the configured architecture and upstream path; keep x86_64/aarch64 packages in separate repositories.", "SYNC_ARCH_MISMATCH") + } + switch code { + case "SYNC_EMPTY_RESULT": + return result(CategoryConfiguration, "apply include/exclude filters", "No RPM matched the configured architecture and package filters.", + "Review include/exclude rules and confirm the target package exists in the upstream snapshot.", "SYNC_EMPTY_RESULT") + case "SYNC_INSUFFICIENT_SPACE": + return result(CategoryStorage, "preflight repository capacity", "Available repository storage is below the conservative mutation requirement.", + "Free staging, quarantine or trash data, or expand the bind-mounted filesystem before retrying.", "SYNC_INSUFFICIENT_SPACE") + case "REVISION_BUILD_FAILED", "REVISION_PUBLISH_FAILED": + return diagnoseCommon(code, message) + default: + return result(CategoryDownload, "download upstream snapshot", "The remote synchronization command failed before a complete revision was built.", + "Inspect the sanitized command evidence, verify the upstream repository, and retry the entire snapshot.", "SYNC_DOWNLOAD_FAILURE") + } +} + +func diagnoseUpload(code, message string) ruleResult { + if code == "DANGEROUS_ARCHIVE" || hasAny(message, "path traversal", "non-regular entry", "symlink", "archive contains non-rpm") { + return result(CategorySecurity, "inspect upload archive", "The archive violates safe extraction rules or contains unsupported content.", + "Rebuild the tar.gz with regular RPM files only and without absolute paths, links or parent traversal.", "UPLOAD_DANGEROUS_ARCHIVE") + } + if hasAny(message, "bad signature", "not trusted", "unauthorized key", "unauthorized_gpg_key", "unsigned_rpm", "public key not installed", "noke") || code == "BAD_SIGNATURE" { + return result(CategoryGPG, "verify RPM signature", "The RPM signature is invalid or was made by a key not authorized for this repository.", + "Sign the RPM with an authorized key or register the correct public key; the rejected file must remain quarantined.", "UPLOAD_GPG_FAILURE") + } + if hasAny(message, "wrong_arch", "wrong architecture", "architecture") { + return result(CategoryArchitecture, "validate RPM architecture", "The uploaded RPM architecture is not allowed by the repository.", + "Upload a package matching the repository architecture or use a dedicated repository for that architecture.", "UPLOAD_ARCH_MISMATCH") + } + if hasAny(message, "nevra_conflict", "same nevra", "nevra conflict") { + return result(CategoryPackage, "check NEVRA uniqueness", "The same NEVRA already exists with different content.", + "Publish a new release value or remove the existing NEVRA through the product workflow before uploading.", "UPLOAD_NEVRA_CONFLICT") + } + if hasAny(message, "damaged_rpm", "invalid_rpm", "not an rpm", "rpm read failed", "payload digest") { + return result(CategoryPackage, "inspect RPM", "The uploaded file is not a valid, readable RPM or its payload is damaged.", + "Rebuild or re-download the RPM and verify it with rpm -K before uploading again.", "UPLOAD_INVALID_RPM") + } + if code == "SOME_PACKAGES_REJECTED" { + return result(CategoryPackage, "validate uploaded packages", "Some RPMs passed validation while other archive members were rejected.", + "Review each quarantine reason, correct the rejected RPMs, and upload only the corrected packages.", "UPLOAD_PARTIAL_REJECTION") + } + if code == "ALL_PACKAGES_REJECTED" { + return result(CategoryPackage, "validate uploaded packages", "Every RPM in the upload was rejected by package policy.", + "Review quarantine evidence for each RPM and correct architecture, integrity, signature or NEVRA issues.", "UPLOAD_ALL_REJECTED") + } + return diagnoseCommon(code, message) +} + +func diagnoseValidation(code, message string) ruleResult { + if hasAny(message, "could not resolve host", "connection refused", "couldn't connect", "failed to download metadata", "cannot download repomd.xml") { + return result(CategoryNetwork, "reach generated repository URL", "DNF could not reach the repository URL generated by OCManager.", + "Verify the generated baseurl, Nginx read-only route, DNS/routing and immutable revision availability.", "DNF_REPOSITORY_UNREACHABLE") + } + if hasAny(message, "no match for argument", "unable to find a match", "no package") { + return result(CategoryPackage, "resolve target package", "The requested package is not present in any enabled OCManager repository.", + "Confirm the package NEVRA and source repository inventory, then sync or upload it before validation.", "DNF_PACKAGE_MISSING") + } + if hasAny(message, "nothing provides", "requires ") { + return result(CategoryDependency, "resolve package dependencies", "A required dependency is absent from the explicitly enabled repositories.", + "Add the provider package to the synchronized repository and rerun validation with only the intended repository IDs enabled.", "DNF_DEPENDENCY_MISSING") + } + if hasAny(message, "conflicting requests", "package conflict", "cannot install the best candidate", "problem with installed package") { + return result(CategoryDependency, "solve dependency transaction", "DNF found incompatible package requirements or installed-package conflicts.", + "Inspect the conflicting NEVRAs and align repository versions; do not enable unrelated system repositories.", "DNF_DEPENDENCY_CONFLICT") + } + if hasAny(message, "bad gpg signature", "gpg check failed", "public key is not installed", "signature verification failed") { + return result(CategoryGPG, "verify package signature", "DNF rejected repository content because GPG verification failed.", + "Verify gpgcheck/gpgkey in the generated config and publish the authorized public key before retrying.", "DNF_GPG_FAILURE") + } + if hasAny(message, "repomd.xml", "metadata", "primary.xml", "checksum") { + return result(CategoryMetadata, "load repository metadata", "DNF could not parse or verify the published repository metadata.", + "Validate repomd.xml and referenced checksums for the immutable revision, then rebuild metadata atomically.", "DNF_METADATA_INVALID") + } + return result(CategoryTask, "execute isolated DNF validation", "The isolated DNF validation command failed without a more specific rule match.", + "Inspect the sanitized validation log, exit code, enabled repo IDs and generated configuration.", "DNF_VALIDATION_FAILURE") +} + +func diagnoseCommon(code, message string) ruleResult { + switch code { + case "REVISION_BUILD_FAILED": + return result(CategoryMetadata, "build repository revision", "The immutable revision or repodata could not be assembled.", + "Inspect createrepo_c output and candidate package paths; keep the active revision unchanged.", "REVISION_BUILD_FAILURE") + case "REVISION_PUBLISH_FAILED": + return result(CategoryPublication, "publish immutable revision", "The candidate revision failed validation or atomic publication.", + "Verify manifest checksums, bind-mount permissions and relative symlink support before retrying.", "REVISION_PUBLISH_FAILURE") + case "CANCELED_BY_USER", "CANCELED_AFTER_LEASE_EXPIRY": + return result(CategoryTask, "process cancellation", "The task was canceled by an authorized user before publication.", + "Review the task context and create a retry only when the repository mutation is still required.", "TASK_CANCELED") + case "LEASE_EXPIRED", "LEASE_EXPIRED_RETRY": + return result(CategoryTask, "maintain worker lease", "The worker stopped heartbeating before task completion.", + "Check worker health and task logs; recovery will retry only within the configured attempt limit.", "TASK_LEASE_EXPIRED") + case "GPG_KEY_UNAVAILABLE": + return result(CategoryGPG, "load authorized repository keys", "The repository requires package signature verification but has no usable authorized public key.", + "Register a valid public key for this repository and verify its published URL before retrying.", "GPG_KEY_UNAVAILABLE") + case "QUARANTINE_FAILED": + return result(CategoryStorage, "move rejected content to quarantine", "Rejected content could not be stored safely in quarantine.", + "Verify bind-mount capacity and permissions; do not publish any candidate content.", "QUARANTINE_STORAGE_FAILURE") + } + return result(CategoryTask, defaultStep(message), "The task failed without a more specific deterministic rule match.", + "Inspect the sanitized evidence and task log, correct the underlying condition, then use the product retry action.", "TASK_UNCLASSIFIED_FAILURE") +} + +func result(category, step, cause, action, ruleID string) ruleResult { + return ruleResult{Category: category, Step: step, Cause: cause, Action: action, RuleID: ruleID} +} + +func defaultStep(message string) string { + if strings.TrimSpace(message) == "" { + return "execute repository task" + } + return "execute repository task" +} + +func httpStatus(message string) string { + match := httpStatusPattern.FindStringSubmatch(message) + if len(match) == 2 { + return match[1] + } + return "" +} + +func hasAny(value string, fragments ...string) bool { + for _, fragment := range fragments { + if strings.Contains(value, fragment) { + return true + } + } + return false +} + +func Sanitize(value string) string { + value = strings.TrimSpace(value) + value = credentialURLPattern.ReplaceAllString(value, `${1}[REDACTED]@`) + value = bearerPattern.ReplaceAllString(value, `${1} [REDACTED]`) + value = secretPattern.ReplaceAllString(value, `${1}${2}[REDACTED]`) + return strings.Join(strings.Fields(value), " ") +} + +func truncate(value string, maximum int) string { + if len(value) <= maximum { + return value + } + return value[:maximum] +} diff --git a/manager/backend/services/footstone/domain/repositoryDiagnosisService/service_test.go b/manager/backend/services/footstone/domain/repositoryDiagnosisService/service_test.go new file mode 100644 index 0000000000000000000000000000000000000000..49588b42bc42483ed446f35518a3e43a415b98fb --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryDiagnosisService/service_test.go @@ -0,0 +1,178 @@ +package repositoryDiagnosisService + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +type diagnosisRuleCase struct { + name, taskType, code, message, category, rule string +} + +func diagnosisRuleCases() []diagnosisRuleCase { + return []diagnosisRuleCase{ + {"node offline", request.RepositoryTaskTypeSync, "REPOSITORY_NODE_OFFLINE", "heartbeat expired", CategoryNetwork, "NODE_OFFLINE"}, + {"node unauthorized", request.RepositoryTaskTypeSync, "REPOSITORY_NODE_UNAUTHORIZED", "mTLS rejected", CategorySecurity, "NODE_MTLS_UNAUTHORIZED"}, + {"node stale preflight", request.RepositoryTaskTypeUpload, "REPOSITORY_NODE_PRECHECK_STALE", "preflight expired", CategoryConfiguration, "NODE_PREFLIGHT_STALE"}, + {"node insufficient space", request.RepositoryTaskTypeSync, "REPOSITORY_NODE_INSUFFICIENT_SPACE", "free_bytes=50 required_free_bytes=165", CategoryStorage, "NODE_INSUFFICIENT_SPACE"}, + {"node inode exhausted", request.RepositoryTaskTypeUpload, "REPOSITORY_NODE_INODE_EXHAUSTED", "free_inodes=100 required_inodes=10000", CategoryStorage, "NODE_INODE_EXHAUSTED"}, + {"node tool missing", request.RepositoryTaskTypeSync, "REPOSITORY_NODE_TOOL_MISSING", "required tool createrepo_c is unavailable", CategoryConfiguration, "NODE_TOOL_MISSING"}, + {"node filesystem unsupported", request.RepositoryTaskTypeSync, "REPOSITORY_NODE_FS_UNSUPPORTED", "filesystem tmpfs is unsupported", CategoryStorage, "NODE_FILESYSTEM_UNSUPPORTED"}, + {"sync dns", request.RepositoryTaskTypeSync, "SYNC_DOWNLOAD_FAILED", "Could not resolve host: mirror.invalid", CategoryNetwork, "SYNC_DNS_FAILURE"}, + {"sync http 404", request.RepositoryTaskTypeSync, "SYNC_DOWNLOAD_FAILED", "HTTP error 404 for repomd.xml", CategoryUpstreamHTTP, "SYNC_HTTP_4XX"}, + {"sync http 503", request.RepositoryTaskTypeSync, "SYNC_DOWNLOAD_FAILED", "status code: 503", CategoryUpstreamHTTP, "SYNC_HTTP_5XX"}, + {"sync unreachable", request.RepositoryTaskTypeSync, "SYNC_DOWNLOAD_FAILED", "Connection refused", CategoryNetwork, "SYNC_UPSTREAM_UNREACHABLE"}, + {"sync checksum", request.RepositoryTaskTypeSync, "SYNC_VALIDATION_FAILED", "package checksum mismatch", CategoryChecksum, "SYNC_CHECKSUM_FAILURE"}, + {"sync metadata", request.RepositoryTaskTypeSync, "SYNC_VALIDATION_FAILED", "repomd.xml parse error", CategoryMetadata, "SYNC_METADATA_INVALID"}, + {"sync architecture", request.RepositoryTaskTypeSync, "SYNC_VALIDATION_FAILED", "package architecture mismatch", CategoryArchitecture, "SYNC_ARCH_MISMATCH"}, + {"sync gpg", request.RepositoryTaskTypeSync, "SYNC_GPG_VERIFICATION_FAILED", "UNAUTHORIZED_GPG_KEY", CategoryGPG, "SYNC_GPG_FAILURE"}, + {"sync filters", request.RepositoryTaskTypeSync, "SYNC_EMPTY_RESULT", "no RPM matched", CategoryConfiguration, "SYNC_EMPTY_RESULT"}, + {"upload dangerous archive", request.RepositoryTaskTypeUpload, "DANGEROUS_ARCHIVE", "archive path traversal", CategorySecurity, "UPLOAD_DANGEROUS_ARCHIVE"}, + {"upload bad signature", request.RepositoryTaskTypeUpload, "ALL_PACKAGES_REJECTED", "BAD_SIGNATURE unauthorized key", CategoryGPG, "UPLOAD_GPG_FAILURE"}, + {"upload unsigned", request.RepositoryTaskTypeUpload, "ALL_PACKAGES_REJECTED", "UNSIGNED_RPM:1", CategoryGPG, "UPLOAD_GPG_FAILURE"}, + {"upload architecture", request.RepositoryTaskTypeUpload, "ALL_PACKAGES_REJECTED", "WRONG_ARCH aarch64", CategoryArchitecture, "UPLOAD_ARCH_MISMATCH"}, + {"upload nevra", request.RepositoryTaskTypeUpload, "ALL_PACKAGES_REJECTED", "NEVRA_CONFLICT", CategoryPackage, "UPLOAD_NEVRA_CONFLICT"}, + {"upload damaged", request.RepositoryTaskTypeUpload, "ALL_PACKAGES_REJECTED", "DAMAGED_RPM payload digest", CategoryPackage, "UPLOAD_INVALID_RPM"}, + {"upload partial", request.RepositoryTaskTypeUpload, "SOME_PACKAGES_REJECTED", "accepted=8 rejected=2", CategoryPackage, "UPLOAD_PARTIAL_REJECTION"}, + {"validation unreachable", request.RepositoryTaskTypeValidate, "DNF_VALIDATION_FAILED", "Cannot download repomd.xml: connection refused", CategoryNetwork, "DNF_REPOSITORY_UNREACHABLE"}, + {"validation package missing", request.RepositoryTaskTypeValidate, "DNF_VALIDATION_FAILED", "No match for argument: fixture", CategoryPackage, "DNF_PACKAGE_MISSING"}, + {"validation dependency missing", request.RepositoryTaskTypeValidate, "DNF_VALIDATION_FAILED", "nothing provides libfixture needed by app", CategoryDependency, "DNF_DEPENDENCY_MISSING"}, + {"validation conflict", request.RepositoryTaskTypeValidate, "DNF_VALIDATION_FAILED", "conflicting requests", CategoryDependency, "DNF_DEPENDENCY_CONFLICT"}, + {"validation gpg", request.RepositoryTaskTypeValidate, "DNF_VALIDATION_FAILED", "GPG check FAILED", CategoryGPG, "DNF_GPG_FAILURE"}, + {"validation metadata", request.RepositoryTaskTypeValidate, "DNF_VALIDATION_FAILED", "primary.xml checksum error", CategoryMetadata, "DNF_METADATA_INVALID"}, + {"validation fallback", request.RepositoryTaskTypeValidate, "DNF_VALIDATION_FAILED", "dnf exited with code 1", CategoryTask, "DNF_VALIDATION_FAILURE"}, + {"publication", request.RepositoryTaskTypeUpload, "REVISION_PUBLISH_FAILED", "atomic swap failed", CategoryPublication, "REVISION_PUBLISH_FAILURE"}, + {"gpg key unavailable", request.RepositoryTaskTypeUpload, "GPG_KEY_UNAVAILABLE", "no authorized public key", CategoryGPG, "GPG_KEY_UNAVAILABLE"}, + } +} + +func TestAnalyzeRuleMatrix(t *testing.T) { + now := time.Date(2026, 8, 27, 8, 0, 0, 0, time.UTC) + for _, tt := range diagnosisRuleCases() { + t.Run(tt.name, func(t *testing.T) { + got := Analyze(Context{TaskType: tt.taskType, Status: request.RepositoryTaskStatusFailed, + ErrorCode: tt.code, ErrorMessage: tt.message}, now) + if got == nil || got.FailureCategory != tt.category || got.RuleID != tt.rule || + got.FailedStep == "" || got.LikelyCause == "" || got.RecommendedAction == "" || len(got.Evidence) < 4 { + t.Fatalf("Analyze() = %#v, want category=%s rule=%s", got, tt.category, tt.rule) + } + }) + } +} + +func TestDiagnosisAcceptanceReport(t *testing.T) { + path := strings.TrimSpace(os.Getenv("OCM_REPOSITORY_DIAGNOSIS_REPORT")) + if path == "" { + return + } + type result struct { + Name string `json:"name"` + TaskType string `json:"task_type"` + ErrorCode string `json:"error_code"` + ExpectedCategory string `json:"expected_category"` + ActualCategory string `json:"actual_category"` + ExpectedRule string `json:"expected_rule"` + ActualRule string `json:"actual_rule"` + DurationNS int64 `json:"duration_ns"` + Status string `json:"status"` + } + cases := diagnosisRuleCases() + results := make([]result, 0, len(cases)) + passed := 0 + for _, tt := range cases { + started := time.Now() + got := Analyze(Context{TaskType: tt.taskType, Status: request.RepositoryTaskStatusFailed, + ErrorCode: tt.code, ErrorMessage: tt.message}, started) + item := result{Name: tt.name, TaskType: tt.taskType, ErrorCode: tt.code, + ExpectedCategory: tt.category, ExpectedRule: tt.rule, DurationNS: time.Since(started).Nanoseconds(), + Status: "FAIL"} + if got != nil { + item.ActualCategory, item.ActualRule = got.FailureCategory, got.RuleID + } + if item.ActualCategory == item.ExpectedCategory && item.ActualRule == item.ExpectedRule { + item.Status = "PASS" + passed++ + } + results = append(results, item) + } + report := struct { + SchemaVersion int `json:"schema_version"` + Phase string `json:"phase"` + GeneratedAt time.Time `json:"generated_at"` + Status string `json:"status"` + Total int `json:"total"` + Passed int `json:"passed"` + Failed int `json:"failed"` + Cases []result `json:"cases"` + }{1, "Phase 8 Failure Diagnosis", time.Now().UTC(), "PASS", len(results), passed, len(results) - passed, results} + if report.Failed > 0 { + report.Status = "FAIL" + } + content, err := json.MarshalIndent(report, "", " ") + if err != nil { + t.Fatalf("encode diagnosis report: %v", err) + } + if err = os.MkdirAll(filepath.Dir(path), 0750); err != nil { + t.Fatalf("create diagnosis report directory: %v", err) + } + if err = os.WriteFile(path, append(content, '\n'), 0640); err != nil { + t.Fatalf("write diagnosis report: %v", err) + } + if report.Failed > 0 { + t.Fatalf("diagnosis acceptance report failed %d/%d cases", report.Failed, report.Total) + } +} + +func TestAnalyzeSanitizesEvidenceAndApplyRoundTrips(t *testing.T) { + now := time.Date(2026, 8, 27, 9, 0, 0, 0, time.UTC) + diagnosis := Analyze(Context{ + TaskType: request.RepositoryTaskTypeSync, Status: request.RepositoryTaskStatusFailed, + ErrorCode: "SYNC_DOWNLOAD_FAILED", + ErrorMessage: "Authorization: Bearer abc.def https://alice:password@repo.invalid/path token=topsecret", + CurrentStep: "download metadata password=hidden", + }, now) + if diagnosis == nil { + t.Fatal("Analyze() returned nil") + } + joined := strings.Join(diagnosis.Evidence, " ") + for _, secret := range []string{"abc.def", "alice", "password@", "topsecret", "hidden"} { + if strings.Contains(joined, secret) { + t.Fatalf("diagnosis evidence leaked %q: %s", secret, joined) + } + } + if strings.Count(joined, "[REDACTED]") < 3 { + t.Fatalf("diagnosis evidence was not redacted: %s", joined) + } + task := new(po.RepositoryTask) + if err := Apply(task, diagnosis); err != nil { + t.Fatalf("Apply() error = %v", err) + } + roundTrip := task.StructuredDiagnosis() + if roundTrip == nil || roundTrip.RuleID != diagnosis.RuleID || task.ErrorCode != diagnosis.ErrorCode || + !roundTrip.GeneratedAt.Equal(now) || + len(roundTrip.Evidence) != len(diagnosis.Evidence) { + t.Fatalf("StructuredDiagnosis() = %#v", roundTrip) + } + if err := Apply(task, nil); err != nil || task.StructuredDiagnosis() != nil { + t.Fatalf("Apply(nil) task/error = %#v/%v", task, err) + } +} + +func TestAnalyzeSuccessHasNoDiagnosis(t *testing.T) { + if got := Analyze(Context{TaskType: request.RepositoryTaskTypeSync, + Status: request.RepositoryTaskStatusSuccess}, time.Now()); got != nil { + t.Fatalf("Analyze(success) = %#v", got) + } + failed := Analyze(Context{TaskType: request.RepositoryTaskTypeSync, + Status: request.RepositoryTaskStatusFailed}, time.Now()) + if failed == nil || failed.ErrorCode != "TASK_FAILED" || failed.RuleID != "SYNC_DOWNLOAD_FAILURE" { + t.Fatalf("Analyze(failed without code) = %#v", failed) + } +} diff --git a/manager/backend/services/footstone/domain/repositoryDistributionService/report.go b/manager/backend/services/footstone/domain/repositoryDistributionService/report.go new file mode 100644 index 0000000000000000000000000000000000000000..d84c0ce7113215bf389fd72c3e461d7c116f58f7 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryDistributionService/report.go @@ -0,0 +1,79 @@ +package repositoryDistributionService + +import ( + "fmt" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryDiagnosisService" +) + +type distributionTargetReport struct { + TargetIP string `json:"target_ip"` + AgentID uint64 `json:"agent_id"` + Status string `json:"status"` + AttemptCount int `json:"attempt_count"` + ExitCode int32 `json:"exit_code"` + Output string `json:"output,omitempty"` + OutputRef string `json:"output_ref,omitempty"` + OutputTruncated bool `json:"output_truncated"` + ErrorCode string `json:"error_code,omitempty"` + ErrorMessage string `json:"error_message,omitempty"` + StartedAt *time.Time `json:"started_at,omitempty"` + FinishedAt *time.Time `json:"finished_at,omitempty"` +} + +func (s *service) Export(ctx entity.Context, id string) (*request.RepositoryExport, error) { + value, err := s.Describe(ctx, id) + if err != nil { + return nil, err + } + targets := make([]distributionTargetReport, 0, len(value.Targets)) + passed, failed := 0, 0 + for index := range value.Targets { + target := &value.Targets[index] + if target.Status == request.RepositoryDistributionTargetSuccess { + passed++ + } else if target.Status == request.RepositoryDistributionTargetFailed { + failed++ + } + targets = append(targets, distributionTargetReport{ + TargetIP: target.TargetIP, AgentID: target.AgentID, Status: target.Status, + AttemptCount: target.AttemptCount, ExitCode: target.ExitCode, + Output: repositoryDiagnosisService.Sanitize(target.Output), OutputRef: target.OutputRef, + OutputTruncated: target.OutputTruncated, ErrorCode: target.ErrorCode, + ErrorMessage: repositoryDiagnosisService.Sanitize(target.ErrorMessage), + StartedAt: target.StartedAt, FinishedAt: target.FinishedAt, + }) + } + report := struct { + SchemaVersion string `json:"schema_version"` + GeneratedAt time.Time `json:"generated_at"` + DistributionID string `json:"distribution_id"` + RepositoryID string `json:"repository_id"` + RevisionID string `json:"revision_id"` + RepositoryTaskID string `json:"repository_task_id"` + TMSTaskID string `json:"tms_task_id,omitempty"` + ManifestSHA256 string `json:"manifest_sha256"` + RepomdSHA256 string `json:"repomd_sha256"` + Status string `json:"status"` + ErrorCode string `json:"error_code,omitempty"` + ErrorMessage string `json:"error_message,omitempty"` + Targets []distributionTargetReport `json:"targets"` + Summary map[string]int `json:"summary"` + CreatedAt time.Time `json:"created_at"` + StartedAt *time.Time `json:"started_at,omitempty"` + FinishedAt *time.Time `json:"finished_at,omitempty"` + }{ + SchemaVersion: "ocmanager.repository.distribution/v1", GeneratedAt: time.Now().UTC(), + DistributionID: value.ID, RepositoryID: value.RepositoryID, RevisionID: value.RevisionID, + RepositoryTaskID: value.RepositoryTaskID, TMSTaskID: value.TMSTaskID, + ManifestSHA256: value.ManifestSHA256, RepomdSHA256: value.RepomdSHA256, + Status: value.Status, ErrorCode: value.ErrorCode, + ErrorMessage: repositoryDiagnosisService.Sanitize(value.ErrorMessage), Targets: targets, + Summary: map[string]int{"targets": len(targets), "passed": passed, "failed": failed}, + CreatedAt: value.CreatedAt, StartedAt: value.StartedAt, FinishedAt: value.FinishedAt, + } + return request.NewRepositoryJSONExport(fmt.Sprintf("repository-distribution-%s.json", value.ID), report) +} diff --git a/manager/backend/services/footstone/domain/repositoryDistributionService/report_integration_test.go b/manager/backend/services/footstone/domain/repositoryDistributionService/report_integration_test.go new file mode 100644 index 0000000000000000000000000000000000000000..8a20ec250ca4fc68566868afb4e2bba2debb43b1 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryDistributionService/report_integration_test.go @@ -0,0 +1,104 @@ +//go:build integration + +package repositoryDistributionService + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/google/uuid" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + baseconfig "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorydistributionrepo" +) + +type reportRepositoryManager struct{ value *po.SoftwareRepository } + +func (m reportRepositoryManager) Describe(entity.Context, string) (*po.SoftwareRepository, error) { + return m.value, nil +} + +func TestExportDistributionResultWithMySQL(t *testing.T) { + dsn := os.Getenv("OCM_REPOSITORY_REPORT_DSN") + if dsn == "" { + t.Skip("OCM_REPOSITORY_REPORT_DSN is not set") + } + db, err := database.NewDBClient(baseconfig.AccessPoint{Source: dsn}) + if err != nil { + t.Fatal(err) + } + defer db.Close() + repository := repositorydistributionrepo.New(db) + if err = repository.SyncTables(); err != nil { + t.Fatal(err) + } + distributionID := uuid.NewString() + defer func() { + _ = db.DB().Where("distribution_id = ?", distributionID).Delete(&po.RepositoryDistributionTarget{}).Error + _ = db.DB().Where("id = ?", distributionID).Delete(&po.RepositoryDistribution{}).Error + }() + now := time.Date(2026, 8, 24, 11, 0, 0, 0, time.UTC) + value := &po.RepositoryDistribution{ + ID: distributionID, RepositoryID: uuid.NewString(), ProjectID: 900011, + RevisionID: uuid.NewString(), RepositoryTaskID: uuid.NewString(), TMSTaskID: "t-phase11", + ArtifactMD5: strings.Repeat("a", 32), ManifestSHA256: strings.Repeat("b", 64), + RepomdSHA256: strings.Repeat("c", 64), Status: request.RepositoryDistributionStatusPartialSuccess, + RequestedBy: "admin", TimeoutSeconds: 600, ErrorCode: "TARGET_FAILED", + ErrorMessage: "one target failed token=secret", CreatedAt: now, StartedAt: &now, FinishedAt: &now, + } + targets := []po.RepositoryDistributionTarget{ + {ID: uuid.NewString(), DistributionID: distributionID, TargetIP: "192.0.2.10", AgentID: 10, + Status: request.RepositoryDistributionTargetSuccess, AttemptCount: 1, ExitCode: 0, + Output: "repository distribution result: PASS", CreatedAt: now, UpdatedAt: now}, + {ID: uuid.NewString(), DistributionID: distributionID, TargetIP: "192.0.2.11", AgentID: 11, + Status: request.RepositoryDistributionTargetFailed, AttemptCount: 1, ExitCode: 4, + ErrorCode: "CHECKSUM_FAILED", ErrorMessage: "manifest mismatch password=secret", + CreatedAt: now, UpdatedAt: now}, + } + if err = repository.Create(value, targets, nil); err != nil { + t.Fatal(err) + } + service := &service{repositories: reportRepositoryManager{value: &po.SoftwareRepository{ID: value.RepositoryID}}, + distributions: repository} + export, err := service.Export(entity.NewDefaultContext(), distributionID) + if err != nil { + t.Fatal(err) + } + digest := sha256.Sum256([]byte(export.Content)) + if export.SHA256 != hex.EncodeToString(digest[:]) || strings.Contains(export.Content, "secret") { + t.Fatalf("distribution export digest/redaction failed: %s", export.Content) + } + var report struct { + SchemaVersion string `json:"schema_version"` + RevisionID string `json:"revision_id"` + ManifestSHA256 string `json:"manifest_sha256"` + Summary map[string]int `json:"summary"` + } + if err = json.Unmarshal([]byte(export.Content), &report); err != nil { + t.Fatal(err) + } + if report.SchemaVersion != "ocmanager.repository.distribution/v1" || report.RevisionID != value.RevisionID || + report.ManifestSHA256 != value.ManifestSHA256 || report.Summary["targets"] != 2 || + report.Summary["passed"] != 1 || report.Summary["failed"] != 1 { + t.Fatalf("distribution report=%s", export.Content) + } + if output := os.Getenv("OCM_REPOSITORY_REPORT_OUTPUT"); output != "" { + if err = os.MkdirAll(output, 0750); err != nil { + t.Fatal(err) + } + if err = os.WriteFile(filepath.Join(output, export.FileName), []byte(export.Content), 0640); err != nil { + t.Fatal(err) + } + } + t.Logf("distribution_sha256=%s revision_id=%s targets=2", export.SHA256, report.RevisionID) +} diff --git a/manager/backend/services/footstone/domain/repositoryDistributionService/service.go b/manager/backend/services/footstone/domain/repositoryDistributionService/service.go new file mode 100644 index 0000000000000000000000000000000000000000..096bc0129ac367b4337a4c961cdd068e0601840b --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryDistributionService/service.go @@ -0,0 +1,333 @@ +package repositoryDistributionService + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "os" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/google/uuid" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorycontentrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorydistributionrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryTaskService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/tmsclient" +) + +var ( + ErrRevisionNotPublished = errors.New("repository revision is not published") + ErrManifestNotDistributable = errors.New("repository revision manifest is not distributable") + ErrDistributionTargetUnknown = errors.New("distribution target is not registered in TMS") + ErrDistributionTargetOffline = errors.New("distribution target agent is offline") +) + +const maxDistributionManifestBytes = 32 << 20 + +type repositoryManager interface { + Describe(entity.Context, string) (*po.SoftwareRepository, error) +} + +type Service = *service + +type service struct { + db entity.SessionFactory + repositories repositoryManager + tasks repositoryTaskService.Service + content repositorycontentrepo.Repository + distributions repositorydistributionrepo.Repository + store *storage.Store + tms tmsclient.Client + remotePublish bool + httpClient *http.Client +} + +func New(db *database.DBClient, repositories repositoryManager, tasks repositoryTaskService.Service, + store *storage.Store, tms tmsclient.Client, +) Service { + return &service{db: db, repositories: repositories, tasks: tasks, + content: repositorycontentrepo.New(db), distributions: repositorydistributionrepo.New(db), + store: store, tms: tms, httpClient: distributionHTTPClient()} +} + +// ConfigureRemotePublication makes Distribution consume the immutable public +// data plane instead of assuming that Footstone mounts repository storage. +func (s *service) ConfigureRemotePublication() { s.remotePublish = true } + +func (s *service) SyncTables() error { return s.distributions.SyncTables() } + +func (s *service) ListNodes(ctx entity.Context) ([]request.RepositoryDistributionNode, error) { + return s.tms.ListNodes(ctx) +} + +func (s *service) Create(ctx entity.Context, input request.CreateRepositoryDistributionRequest) ( + distribution *po.RepositoryDistribution, task *po.RepositoryTask, err error, +) { + repository, err := s.repositories.Describe(ctx, strings.TrimSpace(input.RepositoryID)) + if err != nil { + return nil, nil, err + } + input.RevisionID = strings.TrimSpace(input.RevisionID) + if input.RevisionID == "" { + input.RevisionID = repository.ActiveRevisionID + } + if uuid.Validate(input.RevisionID) != nil { + return nil, nil, fmt.Errorf("revision_id must be a UUID") + } + revision, err := s.content.GetRevision(repository.ID, input.RevisionID, nil) + if err != nil { + return nil, nil, err + } + if revision.Status != request.RepositoryRevisionStatusPublished || revision.PublishedAt == nil || + revision.ManifestSHA256 == "" || revision.RepomdSHA256 == "" { + return nil, nil, ErrRevisionNotPublished + } + revisionURL, err := s.revisionURL(repository, revision) + if err != nil { + return nil, nil, err + } + if err = s.verifyManifest(repository, revision, revisionURL); err != nil { + return nil, nil, err + } + nodes, err := s.tms.ListNodes(ctx) + if err != nil { + return nil, nil, err + } + nodeByIP := make(map[string]request.RepositoryDistributionNode, len(nodes)) + for _, node := range nodes { + nodeByIP[node.IP] = node + } + targetIPs, err := normalizeTargetIPs(input.TargetIPs) + if err != nil { + return nil, nil, err + } + if err = validateDistributionTargets(targetIPs, nodeByIP); err != nil { + return nil, nil, err + } + if input.TimeoutSeconds == 0 { + input.TimeoutSeconds = 600 + } + if input.TimeoutSeconds < 60 || input.TimeoutSeconds > 1800 { + return nil, nil, fmt.Errorf("timeout_seconds must be between 60 and 1800") + } + distributionID := uuid.NewString() + payload, err := json.Marshal(request.RepositoryDistributionPayload{ + DistributionID: distributionID, TargetIPs: targetIPs, RevisionURL: revisionURL, + ManifestSHA256: revision.SnapshotManifestSHA256, RepomdSHA256: revision.RepomdSHA256, + TimeoutSeconds: input.TimeoutSeconds, + }) + if err != nil { + return nil, nil, err + } + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + task, err = s.tasks.EnqueueInSession(ctx, request.EnqueueRepositoryTaskRequest{ + RepositoryID: repository.ID, RevisionID: revision.ID, Type: request.RepositoryTaskTypeDistribute, + PayloadJSON: string(payload), MaxAttempts: 3, + }, tx) + if err != nil { + return nil, nil, err + } + distribution = &po.RepositoryDistribution{ + ID: distributionID, RepositoryID: repository.ID, ProjectID: repository.ProjectID, + RevisionID: revision.ID, RepositoryTaskID: task.ID, + ManifestSHA256: revision.SnapshotManifestSHA256, RepomdSHA256: revision.RepomdSHA256, + Status: request.RepositoryDistributionStatusPending, RequestedBy: ctx.User.LoginName, + TimeoutSeconds: input.TimeoutSeconds, + } + targets := make([]po.RepositoryDistributionTarget, 0, len(targetIPs)) + for _, ip := range targetIPs { + targets = append(targets, po.RepositoryDistributionTarget{ + ID: uuid.NewString(), DistributionID: distributionID, TargetIP: ip, + AgentID: nodeByIP[ip].AgentID, Status: request.RepositoryDistributionTargetPending, + }) + } + if err = s.distributions.Create(distribution, targets, tx); err != nil { + return nil, nil, err + } + distribution.Targets = targets + return distribution, task, nil +} + +func (s *service) Describe(ctx entity.Context, id string) (*po.RepositoryDistribution, error) { + if uuid.Validate(strings.TrimSpace(id)) != nil { + return nil, fmt.Errorf("distribution_id must be a UUID") + } + value, err := s.distributions.Get(id, nil) + if err != nil { + return nil, err + } + if _, err = s.repositories.Describe(ctx, value.RepositoryID); err != nil { + return nil, err + } + return value, nil +} + +// DescribeInternal is reserved for authenticated repository-node operations. +func (s *service) DescribeInternal(id string) (*po.RepositoryDistribution, error) { + if uuid.Validate(strings.TrimSpace(id)) != nil { + return nil, fmt.Errorf("distribution_id must be a UUID") + } + return s.distributions.Get(id, nil) +} + +func (s *service) List(ctx entity.Context, filter request.ListRepositoryDistributionsRequest) ( + []po.RepositoryDistribution, request.PageResponse, error, +) { + repository, err := s.repositories.Describe(ctx, strings.TrimSpace(filter.RepositoryID)) + if err != nil { + return nil, request.PageResponse{}, err + } + filter.RepositoryID = repository.ID + return s.distributions.List(filter, nil) +} + +func (s *service) revisionURL(repository *po.SoftwareRepository, revision *po.RepositoryRevision) (string, error) { + if !s.remotePublish { + return s.store.PublicRevisionURL(repository.PublishKey, revision.ID) + } + if revision.StorageNodeID == "" || revision.StorageNodeID != repository.PrimaryStorageNodeID { + return "", fmt.Errorf("%w: revision is not located on the repository primary storage node", + ErrManifestNotDistributable) + } + value, err := url.Parse(strings.TrimSpace(repository.PublishBaseURLSnapshot)) + if err != nil || (value.Scheme != "http" && value.Scheme != "https") || value.Host == "" || + value.User != nil || value.RawQuery != "" || value.Fragment != "" { + return "", fmt.Errorf("%w: publish base URL is invalid", ErrManifestNotDistributable) + } + if repository.PublishKey == "" || uuid.Validate(revision.ID) != nil { + return "", fmt.Errorf("%w: publication identity is invalid", ErrManifestNotDistributable) + } + value.Path = strings.TrimRight(value.Path, "/") + "/" + url.PathEscape(repository.PublishKey) + + "/revisions/" + url.PathEscape(revision.ID) + value.RawPath = "" + return value.String(), nil +} + +func (s *service) verifyManifest(repository *po.SoftwareRepository, revision *po.RepositoryRevision, + revisionURL string, +) error { + var body []byte + var err error + if s.remotePublish { + body, err = s.fetchManifest(revisionURL + "/manifest.json") + } else { + manifestPath, pathErr := s.store.Resolve(filepath.ToSlash(filepath.Join(revision.RelativePath, + "manifest.json"))) + if pathErr != nil { + return pathErr + } + body, err = os.ReadFile(manifestPath) + } + if err != nil { + return fmt.Errorf("%w: read manifest: %v", ErrManifestNotDistributable, err) + } + digest := sha256.Sum256(body) + if hex.EncodeToString(digest[:]) != revision.SnapshotManifestSHA256 { + return fmt.Errorf("%w: manifest checksum mismatch", ErrManifestNotDistributable) + } + var manifest struct { + SchemaVersion int `json:"schema_version"` + RepositoryID string `json:"repository_id"` + RevisionID string `json:"revision_id"` + Packages []struct { + Path string `json:"path"` + SHA256 string `json:"sha256"` + } `json:"packages"` + } + if err = json.Unmarshal(body, &manifest); err != nil || manifest.SchemaVersion < 2 || + manifest.RepositoryID != repository.ID || manifest.RevisionID != revision.ID { + return fmt.Errorf("%w: schema v2 manifest required", ErrManifestNotDistributable) + } + for _, item := range manifest.Packages { + if item.Path == "" || item.SHA256 == "" { + return fmt.Errorf("%w: package path or digest missing", ErrManifestNotDistributable) + } + } + return nil +} + +func (s *service) fetchManifest(source string) ([]byte, error) { + request, err := http.NewRequest(http.MethodGet, source, nil) + if err != nil { + return nil, err + } + client := *s.httpClient + client.CheckRedirect = func(_ *http.Request, _ []*http.Request) error { + return errors.New("repository manifest redirects are forbidden") + } + response, err := client.Do(request) + if err != nil { + return nil, err + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK { + return nil, fmt.Errorf("GET manifest returned HTTP %d", response.StatusCode) + } + if response.ContentLength > maxDistributionManifestBytes { + return nil, errors.New("repository manifest exceeds the size limit") + } + body, err := io.ReadAll(io.LimitReader(response.Body, maxDistributionManifestBytes+1)) + if err != nil || len(body) == 0 || len(body) > maxDistributionManifestBytes { + return nil, fmt.Errorf("read bounded repository manifest: size=%d error=%v", len(body), err) + } + return body, nil +} + +func distributionHTTPClient() *http.Client { + return &http.Client{Timeout: 30 * time.Second, Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second}).DialContext, + ForceAttemptHTTP2: true, ResponseHeaderTimeout: 15 * time.Second, IdleConnTimeout: 30 * time.Second, + }} +} + +func normalizeTargetIPs(values []string) ([]string, error) { + if len(values) == 0 || len(values) > 100 { + return nil, fmt.Errorf("target_ips must contain between 1 and 100 entries") + } + seen := make(map[string]struct{}, len(values)) + result := make([]string, 0, len(values)) + for _, value := range values { + value = strings.TrimSpace(value) + if net.ParseIP(value) == nil { + return nil, fmt.Errorf("invalid target ip: %s", value) + } + if _, ok := seen[value]; ok { + continue + } + seen[value] = struct{}{} + result = append(result, value) + } + sort.Strings(result) + return result, nil +} + +func validateDistributionTargets(targetIPs []string, + nodeByIP map[string]request.RepositoryDistributionNode, +) error { + for _, ip := range targetIPs { + node, ok := nodeByIP[ip] + if !ok { + return fmt.Errorf("%w: %s", ErrDistributionTargetUnknown, ip) + } + if !node.Connected { + return fmt.Errorf("%w: %s", ErrDistributionTargetOffline, ip) + } + } + return nil +} diff --git a/manager/backend/services/footstone/domain/repositoryDistributionService/service_test.go b/manager/backend/services/footstone/domain/repositoryDistributionService/service_test.go new file mode 100644 index 0000000000000000000000000000000000000000..28bee7e5d9e68b66e3b7bf674eae72d9fac73e4a --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryDistributionService/service_test.go @@ -0,0 +1,106 @@ +package repositoryDistributionService + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +func TestNormalizeTargetIPs(t *testing.T) { + values, err := normalizeTargetIPs([]string{"192.0.2.2", "192.0.2.1", "192.0.2.2"}) + if err != nil { + t.Fatal(err) + } + if len(values) != 2 || values[0] != "192.0.2.1" || values[1] != "192.0.2.2" { + t.Fatalf("normalizeTargetIPs() = %#v", values) + } + if _, err = normalizeTargetIPs([]string{"not-an-ip"}); err == nil { + t.Fatal("normalizeTargetIPs() accepted an invalid IP") + } +} + +func TestValidateDistributionTargetsRequiresRegisteredOnlineAgent(t *testing.T) { + nodes := map[string]request.RepositoryDistributionNode{ + "192.0.2.1": {IP: "192.0.2.1", Connected: true}, + "192.0.2.2": {IP: "192.0.2.2", Connected: false}, + } + if err := validateDistributionTargets([]string{"192.0.2.1"}, nodes); err != nil { + t.Fatal(err) + } + if err := validateDistributionTargets([]string{"192.0.2.2"}, nodes); !errors.Is(err, ErrDistributionTargetOffline) { + t.Fatalf("offline target error = %v", err) + } + if err := validateDistributionTargets([]string{"192.0.2.3"}, nodes); !errors.Is(err, ErrDistributionTargetUnknown) { + t.Fatalf("unknown target error = %v", err) + } +} + +func TestRemoteDistributionReadsAndVerifiesImmutableManifest(t *testing.T) { + repositoryID := "11111111-1111-4111-8111-111111111111" + revisionID := "22222222-2222-4222-8222-222222222222" + nodeID := "33333333-3333-4333-8333-333333333333" + body, err := json.Marshal(map[string]interface{}{ + "schema_version": 3, "repository_id": repositoryID, "revision_id": revisionID, + "packages": []map[string]string{{"path": "package.rpm", "sha256": strings.Repeat("a", 64)}}, + }) + if err != nil { + t.Fatal(err) + } + digest := sha256.Sum256(body) + server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, req *http.Request) { + if req.URL.Path != "/repositories/private/revisions/"+revisionID+"/manifest.json" { + http.NotFound(response, req) + return + } + _, _ = response.Write(body) + })) + defer server.Close() + repository := &po.SoftwareRepository{ID: repositoryID, PublishKey: "private", + PrimaryStorageNodeID: nodeID, PublishBaseURLSnapshot: server.URL + "/repositories"} + revision := &po.RepositoryRevision{ID: revisionID, RepositoryID: repositoryID, + StorageNodeID: nodeID, SnapshotManifestSHA256: hex.EncodeToString(digest[:])} + service := &service{remotePublish: true, httpClient: server.Client()} + revisionURL, err := service.revisionURL(repository, revision) + if err != nil { + t.Fatal(err) + } + if err = service.verifyManifest(repository, revision, revisionURL); err != nil { + t.Fatal(err) + } + + revision.SnapshotManifestSHA256 = strings.Repeat("0", 64) + if err = service.verifyManifest(repository, revision, revisionURL); !errors.Is(err, ErrManifestNotDistributable) { + t.Fatalf("manifest digest mismatch error = %v", err) + } + revision.StorageNodeID = "44444444-4444-4444-8444-444444444444" + if _, err = service.revisionURL(repository, revision); !errors.Is(err, ErrManifestNotDistributable) { + t.Fatalf("cross-node revision URL error = %v", err) + } +} + +func TestRemoteDistributionManifestRejectsRedirectAndCredentials(t *testing.T) { + redirect := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, req *http.Request) { + http.Redirect(response, req, "https://example.invalid/manifest.json", http.StatusFound) + })) + defer redirect.Close() + service := &service{remotePublish: true, httpClient: redirect.Client()} + if _, err := service.fetchManifest(redirect.URL); err == nil || !strings.Contains(err.Error(), "redirect") { + t.Fatalf("redirected manifest error = %v", err) + } + repository := &po.SoftwareRepository{PublishKey: "private", + PrimaryStorageNodeID: "33333333-3333-4333-8333-333333333333", + PublishBaseURLSnapshot: "https://user:password@repo.example.test/repositories"} + revision := &po.RepositoryRevision{ID: "22222222-2222-4222-8222-222222222222", + StorageNodeID: repository.PrimaryStorageNodeID} + if _, err := service.revisionURL(repository, revision); !errors.Is(err, ErrManifestNotDistributable) { + t.Fatalf("credential-bearing publication URL error = %v", err) + } +} diff --git a/manager/backend/services/footstone/domain/repositoryErrorCatalog/catalog.go b/manager/backend/services/footstone/domain/repositoryErrorCatalog/catalog.go new file mode 100644 index 0000000000000000000000000000000000000000..8f849e3869453b4b9a329c4da650ff3767d70798 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryErrorCatalog/catalog.go @@ -0,0 +1,136 @@ +package repositoryErrorCatalog + +import "strings" + +// Entry is the single backend authority for Repository user-facing error text. +// Code always remains the original stable English error code. +type Entry struct { + Code string `json:"code"` + ZhMessage string `json:"zh_message"` + ZhRecommendedAction string `json:"zh_recommended_action"` +} + +var entries = map[string]Entry{ + "REPOSITORY_SYSTEM_MANAGED": entry("该仓库用于系统组件安装,不能通过普通仓库操作修改或删除。", "保留受保护的 Node RPM 仓库;需要调整安装来源时,使用仓库节点页面的安装源配置。"), + "REPOSITORY_NODE_SYNC_FORBIDDEN": entry("Node RPM 安装源仅接收私有上传,不允许远程同步。", "通过上传和发布流程更新节点 RPM,不要为系统安装源配置外部上游。"), + "REPOSITORY_NODE_INSTALL_SOURCE_INVALID": entry("仓库节点安装源的配置或系统用途不符合要求。", "选择当前项目、匹配架构且启用的私有上传仓库,并通过安装源配置入口完成绑定。"), + "REPOSITORY_NODE_INSTALL_SOURCE_NOT_FOUND": entry("当前项目和架构尚未配置可用的仓库节点安装源。", "在仓库节点页面绑定并启用对应架构的 Node RPM 安装源,再重新选择版本。"), + "REPOSITORY_NODE_ARTIFACT_INVALID": entry("所选节点 RPM 与当前安装源、架构或不可变发布版本不匹配。", "从当前启用安装源重新发布并登记正确的节点 RPM,确认不可变版本可访问后再安装。"), + "REPOSITORY_NODE_ARTIFACT_NOT_FOUND": entry("当前安装源没有可用且匹配目标架构的节点 RPM 版本。", "刷新版本列表并选择当前启用安装源中的已发布版本,或先上传、发布并登记对应架构的节点 RPM。"), + "REPOSITORY_NODE_REGISTRATION_DENIED": entry("目标 Agent 未通过仓库节点注册授权检查。", "检查当前项目的 Agent 白名单、有效期及架构约束;完成授权后重新创建安装任务。"), + "REPOSITORY_NODE_OFFLINE": entry("仓库节点离线或心跳已过期。", "恢复节点和 Worker 心跳,重新执行节点探测后再重试。"), + "NODE_OFFLINE": entry("仓库节点离线或心跳已过期。", "恢复节点和 Worker 心跳,重新执行节点探测后再重试。"), + "REPOSITORY_NODE_UNAUTHORIZED": entry("管理面与仓库节点之间的 mTLS 身份校验失败。", "核对证书链、TLS Server Name 和证书指纹后重新探测。"), + "NODE_MTLS_UNAUTHORIZED": entry("管理面与仓库节点之间的 mTLS 身份校验失败。", "核对证书链、TLS Server Name 和证书指纹后重新探测。"), + "REPOSITORY_NODE_PRECHECK_STALE": entry("容量预检记录已缺失、过期或与当前节点不匹配。", "重新执行节点容量预检,并使用新的预检结果重试任务。"), + "NODE_PREFLIGHT_STALE": entry("容量预检记录已缺失、过期或与当前节点不匹配。", "重新执行节点容量预检,并使用新的预检结果重试任务。"), + "REPOSITORY_NODE_INSUFFICIENT_SPACE": entry("仓库节点可用空间不足,无法安全完成本次变更。", "清理可回收的 staging、quarantine 或 trash,扩容或改选节点后重新预检。"), + "NODE_INSUFFICIENT_SPACE": entry("仓库节点可用空间不足,无法安全完成本次变更。", "清理可回收的 staging、quarantine 或 trash,扩容或改选节点后重新预检。"), + "SYNC_INSUFFICIENT_SPACE": entry("同步所需空间超过仓库节点当前安全余量。", "释放空间或扩容后重新执行预检和同步。"), + "REPOSITORY_NODE_INODE_EXHAUSTED": entry("仓库节点可用 inode 不足。", "清理小文件或选择 inode 充足的文件系统后重新预检。"), + "NODE_INODE_EXHAUSTED": entry("仓库节点可用 inode 不足。", "清理小文件或选择 inode 充足的文件系统后重新预检。"), + "REPOSITORY_NODE_TOOL_MISSING": entry("仓库节点缺少 RPM、元数据、GPG 或传输所需工具。", "按探测结果安装缺失工具并确认版本后重试。"), + "NODE_TOOL_MISSING": entry("仓库节点缺少 RPM、元数据、GPG 或传输所需工具。", "按探测结果安装缺失工具并确认版本后重试。"), + "REPOSITORY_NODE_FS_UNSUPPORTED": entry("仓库数据文件系统不支持原子重命名或相对符号链接。", "迁移到满足原子发布要求的 bind mount 文件系统后重新探测。"), + "NODE_FILESYSTEM_UNSUPPORTED": entry("仓库数据文件系统不支持原子重命名或相对符号链接。", "迁移到满足原子发布要求的 bind mount 文件系统后重新探测。"), + "SYNC_DNS_FAILURE": entry("仓库节点无法解析上游地址。", "检查节点 DNS、上游域名和网络策略后重试完整快照同步。"), + "SYNC_HTTP_4XX": entry("上游仓库返回 HTTP 4xx 响应。", "核对上游路径、访问权限和凭据配置后重试。"), + "SYNC_HTTP_5XX": entry("上游仓库返回 HTTP 5xx 响应。", "确认上游服务恢复后重试完整快照同步。"), + "SYNC_UPSTREAM_UNREACHABLE": entry("仓库节点无法连接上游仓库。", "检查路由、防火墙、代理和上游服务状态后重试。"), + "SYNC_CHECKSUM_FAILURE": entry("下载内容与上游声明的校验和不一致。", "保持当前活动 Revision 不变,刷新上游元数据后重新同步。"), + "SYNC_METADATA_INVALID": entry("上游仓库元数据缺失或格式无效。", "修复或重建上游 repodata 后重新同步。"), + "SYNC_ARCH_MISMATCH": entry("同步内容与仓库架构不匹配。", "核对仓库架构和上游路径,按架构拆分仓库后重试。"), + "SYNC_GPG_FAILURE": entry("同步包未通过仓库 GPG 策略校验。", "修复上游签名或登记正确公钥后重新执行完整同步。"), + "SYNC_EMPTY_RESULT": entry("没有软件包匹配当前架构和 include/exclude 条件。", "检查过滤条件及上游包清单后重新同步。"), + "SYNC_DOWNLOAD_FAILURE": entry("远程同步在完整 Revision 构建前失败。", "查看已脱敏日志,确认上游与网络状态后重试完整快照。"), + "UPLOAD_DANGEROUS_ARCHIVE": entry("上传归档包含不安全路径、链接或不支持的内容。", "重新制作仅含普通 RPM 文件的 tar.gz 后上传。"), + "UPLOAD_GPG_FAILURE": entry("上传 RPM 的签名无效或签名密钥未获授权。", "使用授权密钥重新签名,或登记正确公钥后再次上传。"), + "UPLOAD_ARCH_MISMATCH": entry("上传 RPM 的架构不符合仓库架构。", "上传匹配架构的软件包,或使用对应架构的独立仓库。"), + "UPLOAD_NEVRA_CONFLICT": entry("相同 NEVRA 已存在但文件内容不同。", "提升 Release 或通过产品流程处理已有 NEVRA 后再上传。"), + "UPLOAD_INVALID_RPM": entry("上传文件不是有效 RPM,或 RPM 内容已经损坏。", "使用 rpm -K 校验并重新构建或下载 RPM 后上传。"), + "UPLOAD_PARTIAL_REJECTION": entry("归档中部分 RPM 已接收,部分 RPM 被策略拒绝。", "查看隔离原因,修复被拒绝的软件包后仅重新上传这些包。"), + "UPLOAD_ALL_REJECTED": entry("本次上传的全部 RPM 都被仓库策略拒绝。", "逐项检查完整性、架构、签名和 NEVRA 冲突后重新上传。"), + "UPLOAD_OBJECT_MISSING": entry("任务引用的上传对象已不存在,无法继续验证或发布。", "根据上传 ID 和任务日志核对隔离、清理及历史重试记录;重新上传原文件,不要反复重试已失效的对象引用。"), + "UPLOAD_OBJECT_UNREADABLE": entry("仓库节点无法读取任务引用的上传对象。", "检查任务日志中的原始读取错误、对象类型和存储权限,确认文件可读且完整后再处理。"), + "UPLOAD_OBJECT_MISMATCH": entry("上传对象的实际长度或 SHA-256 与任务冻结信息不一致。", "核对上传 ID、节点与任务快照,保留原始文件及日志;必要时重新上传,不要通过改名或关闭校验绕过。"), + "DNF_REPOSITORY_UNREACHABLE": entry("DNF 无法访问 OCManager 发布的仓库地址。", "核对生成的 baseurl、只读发布路径和网络连通性。"), + "DNF_PACKAGE_MISSING": entry("启用的 OCManager 仓库中不存在目标软件包。", "核对 NEVRA 和仓库清单,先同步或上传目标包。"), + "DNF_DEPENDENCY_MISSING": entry("明确启用的仓库集合中缺少必要依赖。", "将依赖包加入同步仓,并仅启用预期仓库重新验证。"), + "DNF_DEPENDENCY_CONFLICT": entry("DNF 发现版本要求或已安装软件包冲突。", "核对冲突 NEVRA 并统一仓库版本后重新验证。"), + "DNF_GPG_FAILURE": entry("DNF 因 GPG 校验失败拒绝安装。", "核对 Repo 配置中的 gpgcheck、gpgkey 和已发布公钥。"), + "DNF_METADATA_INVALID": entry("DNF 无法解析或校验已发布的仓库元数据。", "校验 immutable Revision 的 repomd.xml 和引用校验和后重建元数据。"), + "DNF_VALIDATION_FAILURE": entry("隔离的 DNF 验证未通过,且没有匹配到更具体的规则。", "查看日志中的退出码、启用 Repo ID 和生成配置后处理。"), + "REVISION_BUILD_FAILED": entry("不可变 Revision 或 repodata 构建失败。", "检查 createrepo_c 输出和候选包路径,保持活动 Revision 不变。"), + "REVISION_BUILD_FAILURE": entry("不可变 Revision 或 repodata 构建失败。", "检查 createrepo_c 输出和候选包路径,保持活动 Revision 不变。"), + "REVISION_PUBLISH_FAILED": entry("候选 Revision 校验或原子发布失败。", "检查清单校验、目录权限和相对符号链接能力后重试。"), + "REVISION_PUBLISH_FAILURE": entry("候选 Revision 校验或原子发布失败。", "检查清单校验、目录权限和相对符号链接能力后重试。"), + "CANCELED_BY_USER": entry("任务已由用户取消。", "确认业务仍需要本次变更后,再通过产品重试入口创建新任务。"), + "CANCELED_AFTER_LEASE_EXPIRY": entry("任务在租约过期后被取消。", "检查 Worker 心跳和任务日志,确认状态后再重试。"), + "TASK_CANCELED": entry("任务已取消,未继续发布候选内容。", "查看任务上下文,确认业务需要后再创建重试任务。"), + "LEASE_EXPIRED": entry("Worker 心跳中断,任务租约已过期。", "恢复 Worker 并查看日志;系统仅在允许的重试次数内恢复任务。"), + "LEASE_EXPIRED_RETRY": entry("Worker 心跳中断,任务进入租约恢复流程。", "检查 Worker 健康状态和节点网络。"), + "TASK_LEASE_EXPIRED": entry("Worker 心跳中断,任务租约已过期。", "恢复 Worker 并查看日志;系统仅在允许的重试次数内恢复任务。"), + "GPG_KEY_UNAVAILABLE": entry("仓库需要签名校验,但没有可用的授权公钥。", "登记有效公钥并确认发布地址可访问后重试。"), + "GPG_KEY_SYNC_REQUIRED": entry("仓库签名策略所需的授权公钥尚未在执行节点就绪。", "检查仓库授权公钥登记、节点密钥同步和访问权限;确认密钥就绪后重新上传或同步,不要关闭 GPG 校验。"), + "QUARANTINE_FAILED": entry("被拒绝内容无法安全写入隔离区。", "检查仓库节点容量和目录权限,禁止发布候选内容。"), + "QUARANTINE_STORAGE_FAILURE": entry("被拒绝内容无法安全写入隔离区。", "检查仓库节点容量和目录权限,禁止发布候选内容。"), + "REPOSITORY_TASK_LOG_EXPIRED": entry("任务完整日志已超过保留期。", "使用任务摘要和结构化诊断排查,必要时重新执行可复现任务。"), + "PACKAGE_INSTALL_DISPATCH_FAILED": entry("软件包安装任务未能下发到目标 Agent。", "检查 TMS、Agent 在线状态和目标权限后重试失败目标。"), + "PACKAGE_INSTALL_TARGET_FAILED": entry("目标节点的软件包安装事务失败。", "查看目标结果与已脱敏 DNF 日志,修复源、依赖或 GPG 问题后重试。"), + "TMS_TARGET_RESULT_MISSING": entry("TMS 任务已结束,但缺少目标节点执行结果。", "核对 TMS 任务与 Agent 回传状态,确认目标状态后再决定是否重试。"), + "DISTRIBUTION_TARGET_FAILURE": entry("不可变 Revision 分发到目标节点失败。", "查看目标节点校验和、空间和原子切换日志,仅重试失败目标。"), + "DISTRIBUTION_EVIDENCE_MISSING": entry("分发任务缺少可验证的目标结果或清单证据。", "检查目标 Agent 回传内容和 manifest 摘要,补齐证据后重新分发。"), + "MIGRATION_MANIFEST_INVALID": entry("仓库迁移清单缺失或校验失败。", "重新生成绑定 immutable Revision 的迁移清单并校验 SHA-256。"), + "MIGRATION_TRANSFER_FAILED": entry("仓库内容传输未完成。", "检查源节点、目标节点和网络,再从受控 staging 重试迁移。"), + "REPOSITORY_NODE_USAGE_SCAN_FAILED": entry("仓库节点无法统计 Repository 实际磁盘占用。", "检查 repository-data 权限和文件系统状态后重新执行节点探测。"), + "REPOSITORY_METADATA_SIGNING_KEY_INVALID": entry("仓库元数据签名密钥不可用或状态无效。", "在仓库节点登记有效 key_ref 和公开公钥后重新发布新 Revision。"), + "TASK_UNCLASSIFIED_FAILURE": entry("仓库任务执行失败,当前错误未匹配到更具体的分类。", "保留原始错误码和技术日志,检查任务上下文后再决定是否重试。"), +} + +// aliases normalizes historical worker codes and deterministic diagnosis rule +// IDs without changing the error code returned to API clients. +var codeAliases = map[string]string{ + "SYNC_DOWNLOAD_FAILED": "SYNC_DOWNLOAD_FAILURE", + "SYNC_GPG_VERIFICATION_FAILED": "SYNC_GPG_FAILURE", + "DANGEROUS_ARCHIVE": "UPLOAD_DANGEROUS_ARCHIVE", + "BAD_SIGNATURE": "UPLOAD_GPG_FAILURE", + "UNAUTHORIZED_GPG_KEY": "UPLOAD_GPG_FAILURE", + "WRONG_ARCH": "UPLOAD_ARCH_MISMATCH", + "NEVRA_CONFLICT": "UPLOAD_NEVRA_CONFLICT", + "INVALID_RPM": "UPLOAD_INVALID_RPM", + "SOME_PACKAGES_REJECTED": "UPLOAD_PARTIAL_REJECTION", + "ALL_PACKAGES_REJECTED": "UPLOAD_ALL_REJECTED", + "REVISION_ACTIVATION_FAILED": "REVISION_PUBLISH_FAILURE", + "TASK_FAILED": "TASK_UNCLASSIFIED_FAILURE", + "TASK_EXECUTION_FAILED": "TASK_UNCLASSIFIED_FAILURE", +} + +func entry(message, action string) Entry { + return Entry{ZhMessage: message, ZhRecommendedAction: action} +} + +// Resolve accepts the original error code followed by optional deterministic +// diagnosis rule IDs. Unknown codes retain the original technical identity. +func Resolve(code string, aliases ...string) Entry { + original := strings.ToUpper(strings.TrimSpace(code)) + candidates := append([]string{original}, aliases...) + for _, candidate := range candidates { + key := strings.ToUpper(strings.TrimSpace(candidate)) + if canonical, ok := codeAliases[key]; ok { + key = canonical + } + if value, ok := entries[key]; ok { + value.Code = original + if value.Code == "" { + value.Code = key + } + return value + } + } + if original == "" { + original = "REPOSITORY_UNKNOWN_ERROR" + } + return Entry{Code: original, + ZhMessage: "仓库任务执行失败,当前错误未匹配到已知分类。", + ZhRecommendedAction: "保留原始错误码和技术日志,检查任务上下文后再决定是否重试。"} +} diff --git a/manager/backend/services/footstone/domain/repositoryErrorCatalog/catalog_test.go b/manager/backend/services/footstone/domain/repositoryErrorCatalog/catalog_test.go new file mode 100644 index 0000000000000000000000000000000000000000..11ed99b3876059e5c5fc05436677fddbcc78d27e --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryErrorCatalog/catalog_test.go @@ -0,0 +1,72 @@ +package repositoryErrorCatalog + +import "testing" + +func TestResolveKeepsOriginalCodeAndUsesAlias(t *testing.T) { + value := Resolve("sync_download_failed", "SYNC_DNS_FAILURE") + if value.Code != "SYNC_DOWNLOAD_FAILED" || value.ZhMessage == "" || value.ZhRecommendedAction == "" { + t.Fatalf("Resolve() = %#v", value) + } +} + +func TestResolveNormalizesHistoricalCodeWithoutReplacingIt(t *testing.T) { + value := Resolve("SYNC_DOWNLOAD_FAILED") + if value.Code != "SYNC_DOWNLOAD_FAILED" || value.ZhMessage == "" || + value.ZhMessage == "仓库任务执行失败,当前错误未匹配到已知分类。" { + t.Fatalf("Resolve() = %#v", value) + } +} + +func TestResolveUnknownKeepsTechnicalIdentity(t *testing.T) { + value := Resolve("VENDOR_NEW_ERROR") + if value.Code != "VENDOR_NEW_ERROR" || value.ZhMessage == "" || value.ZhRecommendedAction == "" { + t.Fatalf("Resolve() = %#v", value) + } +} + +func TestResolveCoversTaskNodeTargetAndSummaryFailures(t *testing.T) { + for _, code := range []string{ + "REPOSITORY_NODE_OFFLINE", + "PACKAGE_INSTALL_TARGET_FAILED", + "DISTRIBUTION_TARGET_FAILURE", + "REPOSITORY_NODE_USAGE_SCAN_FAILED", + } { + value := Resolve(code) + if value.Code != code || value.ZhMessage == "" || value.ZhRecommendedAction == "" { + t.Fatalf("Resolve(%q) = %#v", code, value) + } + } +} + +func TestResolveCoversUploadObjectAndGPGPreparationFailures(t *testing.T) { + for _, code := range []string{ + "GPG_KEY_SYNC_REQUIRED", + "UPLOAD_OBJECT_MISSING", + "UPLOAD_OBJECT_UNREADABLE", + "UPLOAD_OBJECT_MISMATCH", + } { + value := Resolve(code) + if value.Code != code || value.ZhMessage == "" || value.ZhRecommendedAction == "" || + value.ZhMessage == "仓库任务执行失败,当前错误未匹配到已知分类。" { + t.Fatalf("Resolve(%q) = %#v", code, value) + } + } +} + +func TestResolveCoversNodeRPMProtectionAndInstallSourceFailures(t *testing.T) { + for _, code := range []string{ + "REPOSITORY_SYSTEM_MANAGED", + "REPOSITORY_NODE_SYNC_FORBIDDEN", + "REPOSITORY_NODE_INSTALL_SOURCE_INVALID", + "REPOSITORY_NODE_INSTALL_SOURCE_NOT_FOUND", + "REPOSITORY_NODE_ARTIFACT_INVALID", + "REPOSITORY_NODE_ARTIFACT_NOT_FOUND", + "REPOSITORY_NODE_REGISTRATION_DENIED", + } { + value := Resolve(code) + if value.Code != code || value.ZhMessage == "" || value.ZhRecommendedAction == "" || + value.ZhMessage == Resolve("UNRECOGNIZED_NODE_RPM_ERROR").ZhMessage { + t.Fatalf("Resolve(%q) = %#v", code, value) + } + } +} diff --git a/manager/backend/services/footstone/domain/repositoryInventoryService/service.go b/manager/backend/services/footstone/domain/repositoryInventoryService/service.go new file mode 100644 index 0000000000000000000000000000000000000000..9b2e8cb4b151dd4a74b10f1bdeb6cfc8e1e84b9f --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryInventoryService/service.go @@ -0,0 +1,478 @@ +package repositoryInventoryService + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "sort" + "strconv" + "strings" + "time" + "unicode" + + "github.com/google/uuid" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositoryinventoryrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorypackageinstallrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryTaskService" +) + +const ( + previewTTL = 10 * time.Minute + maximumInventoryBytes = 10 << 20 + maximumImportItems = 100 +) + +var ( + ErrInventorySecret = errors.New("INVENTORY_SECRET_DETECTED: inventory contains a forbidden secret field") + ErrInventorySchema = errors.New("INVENTORY_SCHEMA_UNSUPPORTED: inventory schema is not supported") + ErrInventoryPreview = errors.New("INVENTORY_PREVIEW_NOT_READY: inventory preview contains errors") + ErrInventoryExpired = errors.New("PREVIEW_EXPIRED: inventory import preview has expired") + ErrInventoryChanged = errors.New("PREVIEW_CHANGED: inventory import preview or project repositories changed") + ErrInventoryForbidden = errors.New("PREVIEW_FORBIDDEN: inventory import preview belongs to another user") +) + +type inventoryDocument struct { + SchemaVersion string `json:"schema_version"` + Repositories []inventoryRepositoryDefinition `json:"repositories"` +} + +type inventoryRepositoryDefinition struct { + ID string `json:"id,omitempty"` + SystemProduct string `json:"system_product"` + ProductVersion string `json:"product_version"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Architecture string `json:"architecture"` + Type string `json:"type"` + UpstreamURL string `json:"upstream_url,omitempty"` + AllowUpload bool `json:"allow_upload"` + IncludePackages []string `json:"include_packages,omitempty"` + ExcludePackages []string `json:"exclude_packages,omitempty"` + GPGPolicy string `json:"gpg_policy"` + Enabled bool `json:"enabled"` + PrimaryStorageNodeRef string `json:"primary_storage_node_ref,omitempty"` + SyncMode string `json:"sync_mode,omitempty"` + BandwidthLimitBytes int64 `json:"bandwidth_limit_bytes,omitempty"` + MaxParallelDownloads int `json:"max_parallel_downloads,omitempty"` + MetadataSigningKeyRef string `json:"metadata_signing_key_ref,omitempty"` +} + +type inventoryExport struct { + SchemaVersion string `json:"schema_version"` + GeneratedAt time.Time `json:"generated_at"` + ProjectID int64 `json:"source_project_id"` + Repositories []inventoryRepositoryDefinition `json:"repositories"` + Summary map[string]int `json:"summary"` + Excluded []string `json:"excluded"` +} + +type previewPayload struct { + SchemaVersion string `json:"schema_version"` + ProjectID int64 `json:"project_id"` + ResourceVersion string `json:"resource_version"` + ConflictStrategy string `json:"conflict_strategy"` + Items []request.RepositoryInventoryImportItem `json:"items"` + Warnings []string `json:"warnings,omitempty"` +} + +type ConfirmResult struct { + Job *po.RepositoryInventoryImportJob + Repositories []*po.SoftwareRepository +} + +type Service = *service + +type service struct { + db entity.SessionFactory + repositories repositoryService.Service + previews repositorypackageinstallrepo.Repository + jobs repositoryinventoryrepo.Repository + tasks repositoryTaskService.Service + now func() time.Time +} + +func New(db *database.DBClient, repositories repositoryService.Service, tasks repositoryTaskService.Service) Service { + return &service{db: db, repositories: repositories, + previews: repositorypackageinstallrepo.New(db), jobs: repositoryinventoryrepo.New(db), + tasks: tasks, + now: func() time.Time { return time.Now().UTC() }} +} + +func (s *service) SyncTables() error { return s.jobs.SyncTables() } + +func (s *service) Export(ctx entity.Context, projectID int64) (*request.RepositoryExport, error) { + values, err := s.listAll(ctx, projectID) + if err != nil { + return nil, err + } + definitions := make([]inventoryRepositoryDefinition, 0, len(values)) + for index := range values { + value := &values[index] + include, exclude, decodeErr := value.PackageFilters() + if decodeErr != nil { + return nil, decodeErr + } + definitions = append(definitions, inventoryRepositoryDefinition{ + ID: value.ID, SystemProduct: value.SystemProduct, ProductVersion: value.ProductVersion, + Name: value.Name, Description: value.Description, Architecture: value.Architecture, + Type: value.Type, UpstreamURL: value.UpstreamURL, AllowUpload: value.AllowUpload, + IncludePackages: include, ExcludePackages: exclude, GPGPolicy: value.GPGPolicy, + Enabled: value.Enabled, PrimaryStorageNodeRef: value.PrimaryStorageNodeID, + SyncMode: value.SyncMode, BandwidthLimitBytes: value.BandwidthLimitBytes, + MaxParallelDownloads: value.MaxParallelDownloads, + MetadataSigningKeyRef: value.MetadataSigningKeyRef, + }) + } + report := inventoryExport{ + SchemaVersion: request.RepositoryInventorySchemaV2, GeneratedAt: s.now(), ProjectID: projectID, + Repositories: definitions, Summary: map[string]int{"repositories": len(definitions)}, + Excluded: []string{"rpm_content", "revision_content", "private_keys", "secrets", "task_history"}, + } + return request.NewRepositoryJSONExport(fmt.Sprintf("repository-inventory-project-%d.json", projectID), report) +} + +func (s *service) Preview(ctx entity.Context, input request.PreviewRepositoryInventoryImportRequest) ( + *request.RepositoryInventoryImportPreview, error, +) { + if input.ProjectID <= 0 || strings.TrimSpace(input.Content) == "" || len(input.Content) > maximumInventoryBytes { + return nil, fmt.Errorf("project_id and an inventory no larger than %d bytes are required", maximumInventoryBytes) + } + if ctx.User == nil || strings.TrimSpace(ctx.User.LoginName) == "" { + return nil, repositoryService.ErrUnauthenticated + } + strategy := strings.ToUpper(strings.TrimSpace(input.ConflictStrategy)) + switch strategy { + case request.RepositoryInventoryConflictFail, request.RepositoryInventoryConflictSkip, + request.RepositoryInventoryConflictRename: + default: + return nil, fmt.Errorf("unsupported inventory conflict strategy %q", input.ConflictStrategy) + } + var raw interface{} + if err := json.Unmarshal([]byte(input.Content), &raw); err != nil { + return nil, fmt.Errorf("decode repository inventory: %w", err) + } + if path := findForbiddenSecret(raw, "$"); path != "" { + return nil, fmt.Errorf("%w at %s", ErrInventorySecret, path) + } + var document inventoryDocument + if err := json.Unmarshal([]byte(input.Content), &document); err != nil { + return nil, fmt.Errorf("decode repository inventory definitions: %w", err) + } + if document.SchemaVersion != request.RepositoryInventorySchemaV1 && + document.SchemaVersion != request.RepositoryInventorySchemaV2 { + return nil, ErrInventorySchema + } + if len(document.Repositories) == 0 || len(document.Repositories) > maximumImportItems { + return nil, fmt.Errorf("inventory must contain between 1 and %d repositories", maximumImportItems) + } + existing, err := s.listAll(ctx, input.ProjectID) + if err != nil { + return nil, err + } + resourceVersion := repositorySetVersion(existing) + used := make(map[string]struct{}, len(existing)+len(document.Repositories)) + for index := range existing { + used[repositoryIdentity(existing[index].SystemProduct, existing[index].ProductVersion, + existing[index].Name, existing[index].Architecture)] = struct{}{} + } + items := make([]request.RepositoryInventoryImportItem, 0, len(document.Repositories)) + warnings := make([]string, 0) + ready := true + for index := range document.Repositories { + definition := document.Repositories[index] + targetNode := strings.TrimSpace(input.DefaultStorageNodeID) + if mapped, ok := input.StorageNodeMappings[definition.PrimaryStorageNodeRef]; ok { + targetNode = strings.TrimSpace(mapped) + } + targetKey := "" + if definition.MetadataSigningKeyRef != "" { + mapped, ok := input.SigningKeyMappings[definition.MetadataSigningKeyRef] + if !ok { + items = append(items, errorImportItem(definition, "metadata signing key requires an explicit target mapping")) + ready = false + continue + } + targetKey = strings.TrimSpace(mapped) + if targetKey == "" { + warnings = append(warnings, fmt.Sprintf("仓库 %s 未映射元数据签名 Key,导入后不启用元数据签名", definition.Name)) + } + } + spec := definitionToSpec(input.ProjectID, definition, targetNode, targetKey) + item := request.RepositoryInventoryImportItem{SourceRepositoryID: definition.ID, + SourceName: definition.Name, Action: request.RepositoryInventoryActionCreate, Spec: spec} + if targetNode == "" { + item.Action, item.Message, ready = request.RepositoryInventoryActionError, + "repository storage node requires an explicit target mapping", false + items = append(items, item) + continue + } + identity := repositoryIdentity(spec.SystemProduct, spec.ProductVersion, spec.Name, spec.Architecture) + if _, conflict := used[identity]; conflict { + switch strategy { + case request.RepositoryInventoryConflictSkip: + item.Action, item.Message = request.RepositoryInventoryActionSkip, "target repository already exists" + case request.RepositoryInventoryConflictRename: + item.Spec.Name = nextImportName(spec, used) + item.Message = "repository name will be changed to " + item.Spec.Name + default: + item.Action, item.Message, ready = request.RepositoryInventoryActionError, + "target repository already exists", false + } + } + if item.Action == request.RepositoryInventoryActionCreate { + used[repositoryIdentity(item.Spec.SystemProduct, item.Spec.ProductVersion, + item.Spec.Name, item.Spec.Architecture)] = struct{}{} + } + items = append(items, item) + } + payload := previewPayload{SchemaVersion: document.SchemaVersion, ProjectID: input.ProjectID, + ResourceVersion: resourceVersion, ConflictStrategy: strategy, Items: items, Warnings: warnings} + payloadJSON, err := json.Marshal(payload) + if err != nil { + return nil, err + } + previewID := uuid.NewString() + digest := inventoryPreviewDigest(payloadJSON, previewID, ctx.User.LoginName) + now := s.now() + status := request.RepositoryOperationPreviewReady + if !ready { + status = request.RepositoryOperationPreviewFailed + } + preview := &po.RepositoryOperationPreview{ + ID: previewID, ProjectID: input.ProjectID, + OperationType: request.RepositoryOperationInventoryImport, + ResourceID: strconv.FormatInt(input.ProjectID, 10), ResourceVersion: resourceVersion, + PreviewDigest: hex.EncodeToString(digest[:]), PayloadJSON: payloadJSON, Status: status, + ExpiresAt: now.Add(previewTTL), CreatedBy: ctx.User.LoginName, + } + if err = s.previews.CreatePreview(preview, nil); err != nil { + return nil, err + } + return &request.RepositoryInventoryImportPreview{ + ID: preview.ID, SchemaVersion: document.SchemaVersion, PreviewDigest: preview.PreviewDigest, + ResourceVersion: resourceVersion, ExpiresAt: preview.ExpiresAt, Status: preview.Status, + Items: items, Warnings: warnings, + }, nil +} + +func inventoryPreviewDigest(payload []byte, previewID, createdBy string) [sha256.Size]byte { + hash := sha256.New() + _, _ = hash.Write(payload) + _, _ = hash.Write([]byte{0}) + _, _ = hash.Write([]byte(previewID)) + _, _ = hash.Write([]byte{0}) + _, _ = hash.Write([]byte(createdBy)) + var result [sha256.Size]byte + copy(result[:], hash.Sum(nil)) + return result +} + +func (s *service) Confirm(ctx entity.Context, input request.ConfirmRepositoryInventoryImportRequest) ( + result *ConfirmResult, err error, +) { + previewID := strings.TrimSpace(input.PreviewID) + digest := strings.ToLower(strings.TrimSpace(input.PreviewDigest)) + if uuid.Validate(previewID) != nil || len(digest) != sha256.Size*2 { + return nil, ErrInventoryChanged + } + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + + preview, err := s.previews.GetPreview(previewID, true, tx) + if err != nil { + return nil, err + } + if preview.OperationType != request.RepositoryOperationInventoryImport || preview.PreviewDigest != digest { + return nil, ErrInventoryChanged + } + if ctx.User == nil || preview.CreatedBy != ctx.User.LoginName { + return nil, ErrInventoryForbidden + } + if preview.Status == request.RepositoryOperationPreviewConfirmed { + job, jobErr := s.jobs.GetByPreviewID(preview.ID, tx) + return &ConfirmResult{Job: job}, jobErr + } + if preview.Status != request.RepositoryOperationPreviewReady { + return nil, ErrInventoryPreview + } + if !s.now().Before(preview.ExpiresAt) { + return nil, ErrInventoryExpired + } + + var payload previewPayload + if err = json.Unmarshal(preview.PayloadJSON, &payload); err != nil || + payload.ProjectID != preview.ProjectID || payload.ResourceVersion != preview.ResourceVersion { + return nil, ErrInventoryChanged + } + current := make([]po.SoftwareRepository, 0) + if err = tx.Where("project_id = ?", preview.ProjectID).Find(¤t).Error; err != nil || + repositorySetVersion(current) != preview.ResourceVersion { + return nil, ErrInventoryChanged + } + + batch := make([]request.BatchApplySoftwareRepositoryItem, 0, len(payload.Items)) + for index := range payload.Items { + if payload.Items[index].Action == request.RepositoryInventoryActionCreate { + batch = append(batch, request.BatchApplySoftwareRepositoryItem{Spec: payload.Items[index].Spec}) + } + } + created := make([]*po.SoftwareRepository, 0, len(batch)) + if len(batch) > 0 { + results, applyErr := s.repositories.BatchApplyInSession(ctx, batch, tx) + if applyErr != nil { + return nil, applyErr + } + for index := range results { + created = append(created, results[index].Repository) + } + } + + now := s.now() + mappingJSON, _ := json.Marshal(payload.Items) + summaryJSON, _ := json.Marshal(map[string]int{"created": len(created), "skipped": len(payload.Items) - len(batch)}) + taskPayload, _ := json.Marshal(map[string]interface{}{ + "preview_id": preview.ID, "created_count": len(created), "skipped_count": len(payload.Items) - len(batch), + }) + task, err := s.tasks.RecordInventoryImportInSession(ctx, preview.ProjectID, preview.ID, string(taskPayload), tx) + if err != nil { + return nil, err + } + job := &po.RepositoryInventoryImportJob{ + ID: uuid.NewString(), ProjectID: preview.ProjectID, PreviewID: preview.ID, + RepositoryTaskID: task.ID, + SchemaVersion: payload.SchemaVersion, ConflictStrategy: payload.ConflictStrategy, + Status: request.RepositoryInventoryImportSuccess, MappingJSON: mappingJSON, SummaryJSON: summaryJSON, + RequestedBy: ctx.User.LoginName, FinishedAt: &now, + } + if err = s.jobs.Create(job, tx); err != nil { + return nil, err + } + preview.Status, preview.ConfirmedAt = request.RepositoryOperationPreviewConfirmed, &now + if err = s.previews.UpdatePreview(preview, tx); err != nil { + return nil, err + } + return &ConfirmResult{Job: job, Repositories: created}, nil +} + +func (s *service) listAll(ctx entity.Context, projectID int64) ([]po.SoftwareRepository, error) { + values := make([]po.SoftwareRepository, 0) + for page := 1; ; page++ { + items, response, err := s.repositories.List(ctx, request.ListSoftwareRepositoriesRequest{ + ProjectID: projectID, PageRequest: request.PageRequest{PageNumber: page, PageSize: 200}, + }) + if err != nil { + return nil, err + } + values = append(values, items...) + if page >= response.PageCount { + return values, nil + } + } +} + +func definitionToSpec(projectID int64, value inventoryRepositoryDefinition, nodeID, keyRef string) request.SoftwareRepositorySpec { + syncMode := strings.ToUpper(strings.TrimSpace(value.SyncMode)) + if syncMode == "" { + syncMode = request.RepositorySyncModeMirror + } + parallel := value.MaxParallelDownloads + if parallel == 0 { + parallel = 3 + } + return request.SoftwareRepositorySpec{ + ProjectID: projectID, SystemProduct: value.SystemProduct, ProductVersion: value.ProductVersion, + Name: value.Name, Description: value.Description, Architecture: value.Architecture, Type: value.Type, + UpstreamURL: value.UpstreamURL, AllowUpload: value.AllowUpload, + IncludePackages: value.IncludePackages, ExcludePackages: value.ExcludePackages, + GPGPolicy: value.GPGPolicy, Enabled: value.Enabled, PrimaryStorageNodeID: nodeID, + SyncMode: syncMode, BandwidthLimitBytes: value.BandwidthLimitBytes, + MaxParallelDownloads: parallel, MetadataSigningKeyRef: keyRef, + } +} + +func errorImportItem(value inventoryRepositoryDefinition, message string) request.RepositoryInventoryImportItem { + return request.RepositoryInventoryImportItem{SourceRepositoryID: value.ID, SourceName: value.Name, + Action: request.RepositoryInventoryActionError, Message: message} +} + +func repositoryIdentity(product, version, name, architecture string) string { + return strings.ToLower(strings.Join([]string{strings.TrimSpace(product), strings.TrimSpace(version), + strings.TrimSpace(name), strings.TrimSpace(architecture)}, "\x00")) +} + +func nextImportName(spec request.SoftwareRepositorySpec, used map[string]struct{}) string { + base := strings.TrimSpace(spec.Name) + "-imported" + for suffix := 1; suffix <= maximumImportItems+1; suffix++ { + candidate := base + if suffix > 1 { + candidate += "-" + strconv.Itoa(suffix) + } + if _, exists := used[repositoryIdentity(spec.SystemProduct, spec.ProductVersion, + candidate, spec.Architecture)]; !exists { + return candidate + } + } + return base + "-" + uuid.NewString()[:8] +} + +func repositorySetVersion(values []po.SoftwareRepository) string { + rows := make([]string, 0, len(values)) + for index := range values { + value := &values[index] + rows = append(rows, strings.Join([]string{value.ID, value.SystemProduct, value.ProductVersion, + value.Name, value.Architecture, value.UpdatedAt.UTC().Format(time.RFC3339Nano)}, "|")) + } + sort.Strings(rows) + digest := sha256.Sum256([]byte(strings.Join(rows, "\n"))) + return hex.EncodeToString(digest[:]) +} + +func findForbiddenSecret(value interface{}, path string) string { + switch typed := value.(type) { + case map[string]interface{}: + for key, child := range typed { + normalized := normalizeSecretKey(key) + if forbiddenInventoryKey(normalized) { + return path + "." + key + } + if found := findForbiddenSecret(child, path+"."+key); found != "" { + return found + } + } + case []interface{}: + for index, child := range typed { + if found := findForbiddenSecret(child, fmt.Sprintf("%s[%d]", path, index)); found != "" { + return found + } + } + } + return "" +} + +func normalizeSecretKey(value string) string { + var builder strings.Builder + for _, current := range strings.ToLower(value) { + if unicode.IsLetter(current) || unicode.IsDigit(current) { + builder.WriteRune(current) + } + } + return builder.String() +} + +func forbiddenInventoryKey(value string) bool { + for _, forbidden := range []string{"privatekey", "bootstrapToken", "password", "credential", + "apikey", "secret", "accesstoken", "refreshtoken", "tmstoken", "databasepassword"} { + if value == strings.ToLower(forbidden) { + return true + } + } + return false +} diff --git a/manager/backend/services/footstone/domain/repositoryInventoryService/service_test.go b/manager/backend/services/footstone/domain/repositoryInventoryService/service_test.go new file mode 100644 index 0000000000000000000000000000000000000000..cc5e1fd192220158e18bac54ea2f458470827063 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryInventoryService/service_test.go @@ -0,0 +1,68 @@ +package repositoryInventoryService + +import ( + "testing" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +func TestFindForbiddenSecret(t *testing.T) { + safe := map[string]interface{}{ + "metadata_signing_key_ref": "release-2026", + "public_key_url": "https://repo.example/key", + } + if path := findForbiddenSecret(safe, "$"); path != "" { + t.Fatalf("safe logical references rejected at %s", path) + } + unsafe := map[string]interface{}{"repositories": []interface{}{ + map[string]interface{}{"database_password": "do-not-export"}, + }} + if path := findForbiddenSecret(unsafe, "$"); path != "$.repositories[0].database_password" { + t.Fatalf("secret path=%q", path) + } +} + +func TestRepositorySetVersionIsOrderIndependentAndChangeSensitive(t *testing.T) { + now := time.Now().UTC() + first := po.SoftwareRepository{ID: "a", SystemProduct: "OpenCloudOS", ProductVersion: "9.6", + Name: "baseos", Architecture: request.RepositoryArchitectureX8664, UpdatedAt: now} + second := po.SoftwareRepository{ID: "b", SystemProduct: "OpenCloudOS", ProductVersion: "9.6", + Name: "appstream", Architecture: request.RepositoryArchitectureX8664, UpdatedAt: now} + one := repositorySetVersion([]po.SoftwareRepository{first, second}) + two := repositorySetVersion([]po.SoftwareRepository{second, first}) + if one != two { + t.Fatalf("resource version depends on list order: %s != %s", one, two) + } + second.UpdatedAt = now.Add(time.Millisecond) + if changed := repositorySetVersion([]po.SoftwareRepository{first, second}); changed == one { + t.Fatal("resource version did not change after repository update") + } +} + +func TestNextImportNameAvoidsExistingScope(t *testing.T) { + spec := request.SoftwareRepositorySpec{SystemProduct: "OpenCloudOS", ProductVersion: "9.6", + Name: "baseos", Architecture: request.RepositoryArchitectureX8664} + used := map[string]struct{}{ + repositoryIdentity(spec.SystemProduct, spec.ProductVersion, "baseos-imported", spec.Architecture): {}, + repositoryIdentity(spec.SystemProduct, spec.ProductVersion, "baseos-imported-2", spec.Architecture): {}, + } + if got := nextImportName(spec, used); got != "baseos-imported-3" { + t.Fatalf("next import name=%q", got) + } +} + +func TestInventoryPreviewDigestBindsPreviewAndUser(t *testing.T) { + payload := []byte(`{"schema_version":"ocmanager.repository.inventory/v2"}`) + first := inventoryPreviewDigest(payload, "preview-a", "admin") + if second := inventoryPreviewDigest(payload, "preview-b", "admin"); first == second { + t.Fatal("digest does not bind preview identity") + } + if second := inventoryPreviewDigest(payload, "preview-a", "operator"); first == second { + t.Fatal("digest does not bind preview creator") + } + if repeat := inventoryPreviewDigest(payload, "preview-a", "admin"); first != repeat { + t.Fatal("digest is not deterministic for the same preview") + } +} diff --git a/manager/backend/services/footstone/domain/repositoryNodePlatform/architecture.go b/manager/backend/services/footstone/domain/repositoryNodePlatform/architecture.go new file mode 100644 index 0000000000000000000000000000000000000000..3e1081da37e4abdbef9b66c407d6137bb0ebb1bf --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryNodePlatform/architecture.go @@ -0,0 +1,27 @@ +package repositoryNodePlatform + +import ( + "fmt" + "strings" +) + +const ( + ArchitectureX8664 = "x86_64" + ArchitectureARM64 = "aarch64" +) + +// NormalizeArchitecture converts explicit platform aliases to the only two +// canonical Repository Node architecture values. Package architecture values +// such as noarch intentionally do not belong here because a Node always has a +// concrete machine architecture. +func NormalizeArchitecture(value string) (string, error) { + original := value + switch strings.ToLower(strings.TrimSpace(value)) { + case ArchitectureX8664, "amd64", "x64", "x86-64": + return ArchitectureX8664, nil + case ArchitectureARM64, "arm64", "arm64-v8a": + return ArchitectureARM64, nil + default: + return "", fmt.Errorf("unsupported repository node architecture %q", strings.TrimSpace(original)) + } +} diff --git a/manager/backend/services/footstone/domain/repositoryNodePlatform/doc.go b/manager/backend/services/footstone/domain/repositoryNodePlatform/doc.go new file mode 100644 index 0000000000000000000000000000000000000000..168e0f25c018b8dc261f487abe5f1d1253596fdb --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryNodePlatform/doc.go @@ -0,0 +1,12 @@ +// Package repositoryNodePlatform contains platform rules shared by every +// Repository Node boundary. Keeping these rules here prevents Repository, +// Artifact, TMS, Enrollment, and Worker code from maintaining incompatible +// architecture or RPM version mappings. +// +// CompareEVR implements the RPM comparison rules used by OpenCloudOS 9. Its +// compatibility vectors are covered by unit tests, but the P12 integration +// gate must additionally calibrate them against the system RPM implementation +// in a real OpenCloudOS 9 environment (for example rpm.labelCompare through +// the system Python RPM binding or rpmdev-vercmp). Unit tests alone are not +// environment calibration evidence. +package repositoryNodePlatform diff --git a/manager/backend/services/footstone/domain/repositoryNodePlatform/evr.go b/manager/backend/services/footstone/domain/repositoryNodePlatform/evr.go new file mode 100644 index 0000000000000000000000000000000000000000..dd0e9b88f6a2b5fed305de94e7c42b4615ae4683 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryNodePlatform/evr.go @@ -0,0 +1,211 @@ +package repositoryNodePlatform + +import ( + "fmt" + "strconv" + "strings" +) + +// EVR is an RPM Epoch-Version-Release identity. An empty Epoch is the RPM +// default epoch zero. Release may be empty for version-only comparisons. +type EVR struct { + Epoch string + Version string + Release string +} + +// CompareEVR returns -1 when left is older, 0 when both values are equal, and +// 1 when left is newer. Epoch is compared numerically before Version and +// Release are compared with RPM segment semantics. +func CompareEVR(left, right EVR) (int, error) { + leftEpoch, err := parseEpoch(left.Epoch) + if err != nil { + return 0, fmt.Errorf("left epoch: %w", err) + } + rightEpoch, err := parseEpoch(right.Epoch) + if err != nil { + return 0, fmt.Errorf("right epoch: %w", err) + } + if leftEpoch < rightEpoch { + return -1, nil + } + if leftEpoch > rightEpoch { + return 1, nil + } + if strings.TrimSpace(left.Version) == "" || strings.TrimSpace(right.Version) == "" { + return 0, fmt.Errorf("RPM version must not be empty") + } + if result := rpmVersionCompare(left.Version, right.Version); result != 0 { + return result, nil + } + return rpmVersionCompare(left.Release, right.Release), nil +} + +func parseEpoch(value string) (uint64, error) { + value = strings.TrimSpace(value) + if value == "" { + return 0, nil + } + epoch, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return 0, fmt.Errorf("must be an unsigned decimal integer: %w", err) + } + return epoch, nil +} + +// rpmVersionCompare follows rpmvercmp ordering. Separators are ignored, +// numeric segments sort after alphabetic segments, numeric segments ignore +// leading zeroes, '~' sorts before every other value, and '^' sorts after the +// end of a base version but before a following ordinary segment. +func rpmVersionCompare(left, right string) int { + leftIndex, rightIndex := 0, 0 + for leftIndex < len(left) || rightIndex < len(right) { + leftIndex = skipSeparators(left, leftIndex) + rightIndex = skipSeparators(right, rightIndex) + + leftTilde := byteAt(left, leftIndex) == '~' + rightTilde := byteAt(right, rightIndex) == '~' + if leftTilde || rightTilde { + if !leftTilde { + return 1 + } + if !rightTilde { + return -1 + } + leftIndex++ + rightIndex++ + continue + } + + leftCaret := byteAt(left, leftIndex) == '^' + rightCaret := byteAt(right, rightIndex) == '^' + if leftCaret || rightCaret { + if leftIndex >= len(left) { + return -1 + } + if rightIndex >= len(right) { + return 1 + } + if !leftCaret { + return 1 + } + if !rightCaret { + return -1 + } + leftIndex++ + rightIndex++ + continue + } + + if leftIndex >= len(left) || rightIndex >= len(right) { + break + } + + leftNumeric := isASCIIDigit(left[leftIndex]) + rightNumeric := isASCIIDigit(right[rightIndex]) + if leftNumeric != rightNumeric { + if leftNumeric { + return 1 + } + return -1 + } + + leftEnd := scanSegment(left, leftIndex, leftNumeric) + rightEnd := scanSegment(right, rightIndex, rightNumeric) + leftSegment := left[leftIndex:leftEnd] + rightSegment := right[rightIndex:rightEnd] + if leftNumeric { + leftSegment = trimNumericZeroes(leftSegment) + rightSegment = trimNumericZeroes(rightSegment) + if len(leftSegment) < len(rightSegment) { + return -1 + } + if len(leftSegment) > len(rightSegment) { + return 1 + } + } + if result := compareASCII(leftSegment, rightSegment); result != 0 { + return result + } + leftIndex, rightIndex = leftEnd, rightEnd + } + + leftIndex = skipSeparators(left, leftIndex) + rightIndex = skipSeparators(right, rightIndex) + if leftIndex >= len(left) && rightIndex >= len(right) { + return 0 + } + if leftIndex >= len(left) { + if byteAt(right, rightIndex) == '~' { + return 1 + } + return -1 + } + if byteAt(left, leftIndex) == '~' { + return -1 + } + return 1 +} + +func skipSeparators(value string, index int) int { + for index < len(value) && !isASCIIAlphaNumeric(value[index]) && value[index] != '~' && value[index] != '^' { + index++ + } + return index +} + +func scanSegment(value string, index int, numeric bool) int { + for index < len(value) { + if numeric && !isASCIIDigit(value[index]) { + break + } + if !numeric && !isASCIIAlpha(value[index]) { + break + } + index++ + } + return index +} + +func trimNumericZeroes(value string) string { + index := 0 + for index < len(value) && value[index] == '0' { + index++ + } + return value[index:] +} + +func compareASCII(left, right string) int { + minimum := len(left) + if len(right) < minimum { + minimum = len(right) + } + for index := 0; index < minimum; index++ { + if left[index] < right[index] { + return -1 + } + if left[index] > right[index] { + return 1 + } + } + if len(left) < len(right) { + return -1 + } + if len(left) > len(right) { + return 1 + } + return 0 +} + +func byteAt(value string, index int) byte { + if index < 0 || index >= len(value) { + return 0 + } + return value[index] +} + +func isASCIIAlphaNumeric(value byte) bool { return isASCIIAlpha(value) || isASCIIDigit(value) } +func isASCIIAlpha(value byte) bool { + return value >= 'a' && value <= 'z' || value >= 'A' && value <= 'Z' +} +func isASCIIDigit(value byte) bool { return value >= '0' && value <= '9' } diff --git a/manager/backend/services/footstone/domain/repositoryNodePlatform/platform_test.go b/manager/backend/services/footstone/domain/repositoryNodePlatform/platform_test.go new file mode 100644 index 0000000000000000000000000000000000000000..0d8564bf3c05e3e19ef8fcb4abf74e38db6f8b0a --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryNodePlatform/platform_test.go @@ -0,0 +1,101 @@ +package repositoryNodePlatform + +import "testing" + +func TestNormalizeArchitecture(t *testing.T) { + t.Parallel() + tests := []struct { + input string + want string + }{ + {"x86_64", ArchitectureX8664}, + {" AMD64 ", ArchitectureX8664}, + {"x64", ArchitectureX8664}, + {"x86-64", ArchitectureX8664}, + {"aarch64", ArchitectureARM64}, + {"ARM64", ArchitectureARM64}, + {"arm64-v8a", ArchitectureARM64}, + } + for _, test := range tests { + test := test + t.Run(test.input, func(t *testing.T) { + t.Parallel() + got, err := NormalizeArchitecture(test.input) + if err != nil || got != test.want { + t.Fatalf("NormalizeArchitecture(%q) = %q, %v; want %q", test.input, got, err, test.want) + } + }) + } +} + +func TestNormalizeArchitectureRejectsUnknownValues(t *testing.T) { + t.Parallel() + for _, value := range []string{"", "noarch", "i686", "ppc64le", "riscv64", "x86_65"} { + if got, err := NormalizeArchitecture(value); err == nil || got != "" { + t.Errorf("NormalizeArchitecture(%q) = %q, %v; want rejection", value, got, err) + } + } +} + +func TestCompareEVR(t *testing.T) { + t.Parallel() + tests := []struct { + name string + left, right EVR + want int + }{ + {name: "missing epoch equals zero", left: EVR{Version: "1.0", Release: "1"}, + right: EVR{Epoch: "0", Version: "1.0", Release: "1"}, want: 0}, + {name: "epoch precedence", left: EVR{Epoch: "2", Version: "1.0", Release: "1"}, + right: EVR{Epoch: "1", Version: "99.0", Release: "99"}, want: 1}, + {name: "numeric segment newer than alpha", left: EVR{Version: "1.0.1"}, + right: EVR{Version: "1.0a"}, want: 1}, + {name: "numeric magnitude not lexical order", left: EVR{Version: "1.10"}, + right: EVR{Version: "1.9"}, want: 1}, + {name: "leading zero ignored", left: EVR{Version: "1.0002"}, + right: EVR{Version: "1.2"}, want: 0}, + {name: "alpha segment", left: EVR{Version: "1.0b"}, right: EVR{Version: "1.0a"}, want: 1}, + {name: "release decides after equal version", left: EVR{Version: "1.0", Release: "2.oc9"}, + right: EVR{Version: "1.0", Release: "1.oc9"}, want: 1}, + {name: "tilde prerelease older", left: EVR{Version: "1.0~rc1"}, right: EVR{Version: "1.0"}, want: -1}, + {name: "tilde sequence", left: EVR{Version: "1.0~rc2"}, right: EVR{Version: "1.0~rc1"}, want: 1}, + {name: "caret snapshot newer than base", left: EVR{Version: "1.0^git1"}, + right: EVR{Version: "1.0"}, want: 1}, + {name: "caret snapshot older than next segment", left: EVR{Version: "1.0^git1"}, + right: EVR{Version: "1.0.1"}, want: -1}, + {name: "caret sequence", left: EVR{Version: "1.0^git2"}, right: EVR{Version: "1.0^git1"}, want: 1}, + {name: "separators ignored", left: EVR{Version: "1+0_2"}, right: EVR{Version: "1.0.2"}, want: 0}, + {name: "empty release equals empty release", left: EVR{Version: "1.0"}, right: EVR{Version: "1.0"}, want: 0}, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + got, err := CompareEVR(test.left, test.right) + if err != nil || got != test.want { + t.Fatalf("CompareEVR(%+v, %+v) = %d, %v; want %d", test.left, test.right, got, err, test.want) + } + reverse, err := CompareEVR(test.right, test.left) + if err != nil || reverse != -test.want { + t.Fatalf("reverse CompareEVR() = %d, %v; want %d", reverse, err, -test.want) + } + }) + } +} + +func TestCompareEVRRejectsInvalidInput(t *testing.T) { + t.Parallel() + tests := []struct { + left, right EVR + }{ + {left: EVR{Epoch: "-1", Version: "1"}, right: EVR{Version: "1"}}, + {left: EVR{Epoch: "not-a-number", Version: "1"}, right: EVR{Version: "1"}}, + {left: EVR{Version: ""}, right: EVR{Version: "1"}}, + {left: EVR{Version: "1"}, right: EVR{Version: " "}}, + } + for _, test := range tests { + if _, err := CompareEVR(test.left, test.right); err == nil { + t.Errorf("CompareEVR(%+v, %+v) accepted invalid input", test.left, test.right) + } + } +} diff --git a/manager/backend/services/footstone/domain/repositoryNodeService/lifecycle.go b/manager/backend/services/footstone/domain/repositoryNodeService/lifecycle.go new file mode 100644 index 0000000000000000000000000000000000000000..56093f8bdfe9d1920d40b43444b7bd84d92dec13 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryNodeService/lifecycle.go @@ -0,0 +1,908 @@ +package repositoryNodeService + +import ( + "crypto/rand" + "crypto/sha256" + "crypto/subtle" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/google/uuid" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorystoragenoderepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryNodePlatform" +) + +var ( + ErrNodeLifecycleDisabled = errors.New("repository node lifecycle is not configured") + ErrNodeAgentAlreadyBound = errors.New("TMS agent is already bound to a repository node") + ErrBootstrapUnauthorized = errors.New("repository node bootstrap token is invalid or expired") + ErrBootstrapTokenReplay = errors.New("REPOSITORY_NODE_TOKEN_REPLAY") + ErrBootstrapIssuanceRunning = errors.New("repository node certificate issuance is already in progress") + ErrCertificateRenewalTooEarly = errors.New("repository node certificate is not within the renewal window") +) + +const bootstrapIssuanceLease = 2 * time.Minute + +type bootstrapAuthorization struct { + Enrollment *po.RepositoryNodeEnrollment + Node *po.RepositoryStorageNode + LeaseOwner string + CSR []byte + CSRSHA256 string + Certificate *po.RepositoryNodeCertificate +} + +type certificateIssuer interface { + Issue([]byte, string, string) (request.RepositoryIssuedCertificate, error) + CACertificatePEM() []byte +} + +type EnrollmentResult struct { + Node *po.RepositoryStorageNode + Enrollment *po.RepositoryNodeEnrollment + Task *po.RepositoryTask + Target *po.RepositoryTaskTarget + BootstrapToken string + BootstrapExpires time.Time +} + +func (s *service) ConfigureLifecycle(issuer certificateIssuer, policy request.RepositoryNodeLifecyclePolicy) { + if policy.EnrollmentTTL <= 0 { + policy.EnrollmentTTL = 15 * time.Minute + } + if policy.CertificateTTL <= 0 { + policy.CertificateTTL = 90 * 24 * time.Hour + } + if policy.RenewBefore <= 0 { + policy.RenewBefore = 30 * 24 * time.Hour + } + if policy.CertificateGrace <= 0 { + policy.CertificateGrace = 24 * time.Hour + } + s.issuer, s.lifecyclePolicy = issuer, policy +} + +func (s *service) ConfigureArtifacts(root string) error { + root = filepath.Clean(strings.TrimSpace(root)) + if !filepath.IsAbs(root) { + return fmt.Errorf("repository node artifact root must be an absolute path") + } + s.artifactRoot = root + return nil +} + +func (s *service) CreateEnrollment(ctx entity.Context, input request.RepositoryNodeEnrollmentSpec) ( + result *EnrollmentResult, err error, +) { + if s.lifecycle == nil || s.tasks == nil || s.targets == nil || s.issuer == nil { + return nil, ErrNodeLifecycleDisabled + } + input.ExpectedArchitecture, err = repositoryNodePlatform.NormalizeArchitecture(input.ExpectedArchitecture) + if err != nil { + return nil, err + } + if input.TMSAgentID == 0 { + return nil, fmt.Errorf("%w: a TMS agent identity is required", ErrNodeRegistrationDenied) + } + input.Node.CertificateFingerprint = strings.Repeat("0", sha256.Size*2) + input.Node.Enabled = true + nodeSpec, err := normalizeSpec(input.Node) + if err != nil { + return nil, err + } + token, tokenHash, err := newBootstrapToken() + if err != nil { + return nil, err + } + now := s.now() + expires := now.Add(s.lifecyclePolicy.EnrollmentTTL) + labelsJSON, err := json.Marshal(nodeSpec.Labels) + if err != nil { + return nil, fmt.Errorf("encode repository node labels: %w", err) + } + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + if err = s.authorizeProject(ctx, nodeSpec.ProjectID, tx); err != nil { + return nil, err + } + allowlist, err := s.lifecycle.GetAllowlist(nodeSpec.ProjectID, input.TMSAgentID, false, tx) + if err != nil || validateRegistrationAllowlist(allowlist, nodeSpec.ProjectID, input.TMSAgentID, + input.ExpectedArchitecture, now) != nil || + validateAllowlistObservation(allowlist, input.ObservedHostname, input.ObservedIP) != nil { + return nil, ErrNodeRegistrationDenied + } + var existingEnrollment *po.RepositoryNodeEnrollment + if input.TMSAgentID != 0 { + if existingEnrollment, err = s.lifecycle.FindEnrollmentByAgent(input.TMSAgentID, tx); err == nil { + if !canReinstallEnrollment(existingEnrollment.Status) { + return nil, ErrNodeAgentAlreadyBound + } + } else if !errors.Is(err, repositorystoragenoderepo.ErrNodeEnrollmentNotFound) { + return nil, err + } else { + existingEnrollment, err = s.findLegacyEnrollmentForAgentBinding(nodeSpec, input.ExpectedArchitecture, tx) + if err != nil { + return nil, err + } + } + err = nil + } + enrollmentID, taskID := uuid.NewString(), uuid.NewString() + generation := 1 + var node *po.RepositoryStorageNode + if existingEnrollment == nil { + node = &po.RepositoryStorageNode{ + ID: uuid.NewString(), ProjectID: nodeSpec.ProjectID, Name: nodeSpec.Name, Description: nodeSpec.Description, + ControlBaseURL: nodeSpec.ControlBaseURL, PublishBaseURL: nodeSpec.PublishBaseURL, + TLSServerName: nodeSpec.TLSServerName, CertificateFingerprint: "", + Status: request.RepositoryStorageNodeStatusPending, Enabled: true, LabelsJSON: labelsJSON, + Architecture: input.ExpectedArchitecture, CreatedBy: ctx.User.LoginName, + } + if err = s.nodes.CreateNode(node, tx); err != nil { + return nil, err + } + } else { + node, err = s.nodes.GetNodeByID(existingEnrollment.NodeID, tx) + if err != nil || !node.Enabled || node.ProjectID != nodeSpec.ProjectID { + return nil, ErrNodeAgentAlreadyBound + } + generation = existingEnrollment.Generation + 1 + if err = s.lifecycle.InvalidateBootstrapTokens(existingEnrollment.ID, now, tx); err != nil { + return nil, err + } + preserveOnlineIdentity := node.Status == request.RepositoryStorageNodeStatusOnline && + strings.TrimSpace(node.CertificateFingerprint) != "" + node.Name, node.Description = nodeSpec.Name, nodeSpec.Description + node.ControlBaseURL, node.PublishBaseURL = nodeSpec.ControlBaseURL, nodeSpec.PublishBaseURL + node.TLSServerName = nodeSpec.TLSServerName + node.LabelsJSON, node.Architecture = labelsJSON, input.ExpectedArchitecture + if !preserveOnlineIdentity { + node.CertificateFingerprint = "" + node.Status = request.RepositoryStorageNodeStatusPending + } + node.LastErrorCode, node.LastErrorMessage = "", "" + if err = s.nodes.UpdateNode(node, tx); err != nil { + return nil, err + } + } + executor := request.RepositoryTaskExecutorManual + targetType, targetID := "STORAGE_NODE", node.ID + if input.TMSAgentID != 0 { + executor = request.RepositoryTaskExecutorTMS + targetType, targetID = "TMS_AGENT", strconv.FormatUint(input.TMSAgentID, 10) + } + task := &po.RepositoryTask{ + ID: taskID, RootTaskID: taskID, RepositoryID: "", StorageNodeID: node.ID, + ProjectID: nodeSpec.ProjectID, ScopeType: request.RepositoryTaskScopeNode, ScopeID: node.ID, + ExecutorKind: executor, Type: request.RepositoryTaskTypeNodeInstall, + Trigger: request.RepositoryTaskTriggerManual, Status: request.RepositoryTaskStatusPending, + RequiresMutationLock: false, MaxAttempts: 1, RequestedBy: ctx.User.LoginName, + AvailableAt: now, CurrentStep: "waiting for repository node bootstrap", PayloadJSON: "{}", + } + if err = s.tasks.Create(task, tx); err != nil { + return nil, err + } + target := &po.RepositoryTaskTarget{ID: uuid.NewString(), TaskID: taskID, TargetType: targetType, + TargetID: targetID, StorageNodeID: node.ID, TMSAgentID: input.TMSAgentID, + Status: request.RepositoryTaskStatusPending} + if err = s.targets.CreateTarget(target, tx); err != nil { + return nil, err + } + enrollment := &po.RepositoryNodeEnrollment{ + ID: enrollmentID, NodeID: node.ID, ProjectID: nodeSpec.ProjectID, TMSAgentID: input.TMSAgentID, + Generation: generation, Status: request.RepositoryNodeEnrollmentPending, InstallTaskID: taskID, + AllowlistEntryID: allowlist.ID, ArtifactID: strings.TrimSpace(input.ArtifactID), + ExpectedArchitecture: input.ExpectedArchitecture, ExpiresAt: expires, CreatedBy: ctx.User.LoginName, + } + if err = s.lifecycle.CreateEnrollment(enrollment, tx); err != nil { + return nil, err + } + bootstrap := &po.RepositoryNodeBootstrapToken{ + ID: uuid.NewString(), EnrollmentID: enrollmentID, Generation: enrollment.Generation, + TaskID: taskID, TMSAgentID: input.TMSAgentID, TokenHash: tokenHash, ExpiresAt: expires, + } + if err = s.lifecycle.CreateBootstrapToken(bootstrap, tx); err != nil { + return nil, err + } + return &EnrollmentResult{Node: node, Enrollment: enrollment, Task: task, Target: target, + BootstrapToken: token, BootstrapExpires: expires}, nil +} + +// findLegacyEnrollmentForAgentBinding upgrades nodes created before TMS Agent +// identity was persisted. The allowlist has already authenticated the new Agent; +// only an exact project/name/architecture match with an unbound, reinstallable +// enrollment may reuse the stable Node ID. +func (s *service) findLegacyEnrollmentForAgentBinding(nodeSpec request.RepositoryStorageNodeSpec, + architecture string, session entity.Session, +) (*po.RepositoryNodeEnrollment, error) { + nodes, _, err := s.nodes.ListNodes(request.ListRepositoryStorageNodesRequest{ + ProjectID: nodeSpec.ProjectID, Keyword: nodeSpec.Name, + PageRequest: request.PageRequest{PageNumber: 1, PageSize: 100}, + }, session) + if err != nil { + return nil, err + } + for index := range nodes { + node := &nodes[index] + if node.Name != nodeSpec.Name || node.ProjectID != nodeSpec.ProjectID || !node.Enabled || + node.Status != request.RepositoryStorageNodeStatusOnline { + continue + } + nodeArchitecture, normalizeErr := repositoryNodePlatform.NormalizeArchitecture(node.Architecture) + if normalizeErr != nil || nodeArchitecture != architecture { + continue + } + enrollment, findErr := s.lifecycle.FindLatestEnrollmentByNode(node.ID, session) + if findErr != nil { + if errors.Is(findErr, repositorystoragenoderepo.ErrNodeEnrollmentNotFound) { + continue + } + return nil, findErr + } + if enrollment.TMSAgentID == 0 && canReinstallEnrollment(enrollment.Status) { + return enrollment, nil + } + return nil, ErrNodeAgentAlreadyBound + } + return nil, nil +} + +func canReinstallEnrollment(status string) bool { + switch status { + case request.RepositoryNodeEnrollmentFailed, + request.RepositoryNodeEnrollmentExpired, + request.RepositoryNodeEnrollmentBootstrapped, + request.RepositoryNodeEnrollmentRegistered, + request.RepositoryNodeEnrollmentOnline: + return true + default: + return false + } +} + +func (s *service) BootstrapNode(input request.RepositoryNodeBootstrapRequest) ( + *request.RepositoryNodeCertificateBundle, error, +) { + if s.lifecycle == nil || s.tasks == nil || s.targets == nil || s.issuer == nil { + return nil, ErrNodeLifecycleDisabled + } + if uuid.Validate(input.EnrollmentID) != nil || uuid.Validate(input.TaskID) != nil || + uuid.Validate(input.AuthorizationID) != nil || len(input.Token) < 32 || + len(input.CSRPEM) == 0 || len(input.CSRPEM) > 32<<10 { + return nil, ErrBootstrapUnauthorized + } + architecture, err := repositoryNodePlatform.NormalizeArchitecture(input.Architecture) + if err != nil { + return nil, ErrBootstrapUnauthorized + } + input.Architecture = architecture + authorization, err := s.authorizeBootstrap(input) + if err != nil { + return nil, err + } + if authorization.Certificate != nil { + return s.certificateBundle(authorization.Node.ID, authorization.Certificate), nil + } + issued, err := s.issuer.Issue(authorization.CSR, authorization.Node.ID, authorization.Node.TLSServerName) + if err != nil { + s.markBootstrapIssuanceFailed(authorization.Enrollment.ID, authorization.LeaseOwner, err) + return nil, err + } + return s.completeBootstrapAuthorization(authorization, issued) +} + +func (s *service) authorizeBootstrap(input request.RepositoryNodeBootstrapRequest) ( + authorization *bootstrapAuthorization, err error, +) { + hashBytes := sha256.Sum256([]byte(input.Token)) + hash := hex.EncodeToString(hashBytes[:]) + csrDigest := sha256.Sum256(input.CSRPEM) + csrSHA256 := hex.EncodeToString(csrDigest[:]) + now := s.now() + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + token, err := s.lifecycle.GetBootstrapTokenByHashForUpdate(hash, tx) + if err != nil || token.EnrollmentID != input.EnrollmentID || token.TaskID != input.TaskID || + token.TMSAgentID != input.TMSAgentID || token.InvalidatedAt != nil { + return nil, ErrBootstrapUnauthorized + } + enrollment, err := s.lifecycle.GetEnrollmentByIDForUpdate(input.EnrollmentID, tx) + if err != nil || enrollment.InstallTaskID != input.TaskID || enrollment.TMSAgentID != input.TMSAgentID || + enrollment.Generation != token.Generation { + return nil, ErrBootstrapUnauthorized + } + node, err := s.nodes.GetNodeByID(enrollment.NodeID, tx) + if err != nil || !node.Enabled || node.ProjectID != enrollment.ProjectID || + enrollment.ExpectedArchitecture != input.Architecture { + return nil, ErrBootstrapUnauthorized + } + exactAuthorization := enrollment.AuthorizationID != nil && *enrollment.AuthorizationID == input.AuthorizationID && + enrollment.CSRSHA256 == csrSHA256 && enrollment.TMSAgentID == input.TMSAgentID && + enrollment.ProjectID == node.ProjectID && enrollment.ExpectedArchitecture == input.Architecture + if token.ConsumedAt != nil { + if !exactAuthorization || enrollment.AuthorizedAt == nil || + (enrollment.Status != request.RepositoryNodeEnrollmentRegistering && + enrollment.Status != request.RepositoryNodeEnrollmentRegistered && + enrollment.Status != request.RepositoryNodeEnrollmentBootstrapped && + enrollment.Status != request.RepositoryNodeEnrollmentOnline) { + return nil, ErrBootstrapTokenReplay + } + if enrollment.Status == request.RepositoryNodeEnrollmentRegistered || + enrollment.Status == request.RepositoryNodeEnrollmentBootstrapped || + enrollment.Status == request.RepositoryNodeEnrollmentOnline { + certificate, certificateErr := s.lifecycle.FindCertificateByAuthorizationID(input.AuthorizationID, tx) + if certificateErr != nil || certificate.CertificatePEM == "" { + return nil, ErrBootstrapTokenReplay + } + return &bootstrapAuthorization{Enrollment: enrollment, Node: node, Certificate: certificate}, nil + } + if enrollment.IssuanceStatus == request.RepositoryNodeIssuanceIssuing && + enrollment.IssuanceLeaseExpiresAt != nil && now.Before(*enrollment.IssuanceLeaseExpiresAt) { + return nil, ErrBootstrapIssuanceRunning + } + } else { + // Only new authorization competes with allowlist revocation. Exact + // recovery below relies on the persisted identity/CSR authorization. + allowlist, allowlistErr := s.lifecycle.GetAllowlist(enrollment.ProjectID, input.TMSAgentID, true, tx) + if allowlistErr != nil || validateRegistrationAllowlist(allowlist, enrollment.ProjectID, input.TMSAgentID, + input.Architecture, now) != nil || allowlist.ID != enrollment.AllowlistEntryID { + return nil, ErrNodeRegistrationDenied + } + task, taskErr := s.tasks.GetByIDForUpdate(input.TaskID, tx) + if taskErr != nil || task.ProjectID != enrollment.ProjectID || task.Type != request.RepositoryTaskTypeNodeInstall || + (task.Status != request.RepositoryTaskStatusPending && task.Status != request.RepositoryTaskStatusRunning) { + return nil, ErrBootstrapUnauthorized + } + if !now.Before(token.ExpiresAt) || !now.Before(enrollment.ExpiresAt) || + enrollment.Status != request.RepositoryNodeEnrollmentInstalling { + return nil, ErrBootstrapUnauthorized + } + consumedAt := now + token.ConsumedAt = &consumedAt + if err = s.lifecycle.UpdateBootstrapToken(token, tx); err != nil { + return nil, err + } + authorizationID := input.AuthorizationID + enrollment.AuthorizationID = &authorizationID + enrollment.CSRSHA256 = csrSHA256 + enrollment.AuthorizedAt = &now + } + leaseOwner := uuid.NewString() + leaseExpires := now.Add(bootstrapIssuanceLease) + enrollment.Status = request.RepositoryNodeEnrollmentRegistering + enrollment.IssuanceStatus = request.RepositoryNodeIssuanceIssuing + enrollment.IssuanceLeaseOwner = leaseOwner + enrollment.IssuanceLeaseExpiresAt = &leaseExpires + if err = s.lifecycle.UpdateEnrollment(enrollment, tx); err != nil { + return nil, err + } + return &bootstrapAuthorization{Enrollment: enrollment, Node: node, LeaseOwner: leaseOwner, + CSR: append([]byte(nil), input.CSRPEM...), CSRSHA256: csrSHA256}, nil +} + +func (s *service) completeBootstrapAuthorization(authorization *bootstrapAuthorization, + issued request.RepositoryIssuedCertificate, +) (bundle *request.RepositoryNodeCertificateBundle, err error) { + now := s.now() + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + enrollment, err := s.lifecycle.GetEnrollmentByIDForUpdate(authorization.Enrollment.ID, tx) + if err != nil || enrollment.AuthorizationID == nil || authorization.Enrollment.AuthorizationID == nil || + *enrollment.AuthorizationID != *authorization.Enrollment.AuthorizationID || + enrollment.CSRSHA256 != authorization.CSRSHA256 || + enrollment.IssuanceLeaseOwner != authorization.LeaseOwner || + enrollment.IssuanceStatus != request.RepositoryNodeIssuanceIssuing { + return nil, ErrBootstrapTokenReplay + } + grace := now.Add(s.lifecyclePolicy.CertificateGrace) + if err = s.lifecycle.SupersedeActiveCertificates(authorization.Node.ID, grace, tx); err != nil { + return nil, err + } + caPEM := s.issuer.CACertificatePEM() + issuerDigest := sha256.Sum256(caPEM) + certificate := &po.RepositoryNodeCertificate{ + ID: uuid.NewString(), NodeID: authorization.Node.ID, EnrollmentID: enrollment.ID, + AuthorizationID: *enrollment.AuthorizationID, + SerialNumber: issued.SerialNumber, Fingerprint: issued.Fingerprint, + CertificatePEM: string(issued.PEM), CertificateChainPEM: string(caPEM), + IssuerFingerprint: hex.EncodeToString(issuerDigest[:]), + Status: request.RepositoryNodeCertificateActive, NotBefore: issued.NotBefore, NotAfter: issued.NotAfter, + } + if err = s.lifecycle.CreateCertificate(certificate, tx); err != nil { + return nil, err + } + enrollment.Status = request.RepositoryNodeEnrollmentRegistered + enrollment.IssuanceStatus = request.RepositoryNodeIssuanceIssued + enrollment.IssuanceLeaseOwner, enrollment.IssuanceLeaseExpiresAt = "", nil + if err = s.lifecycle.UpdateEnrollment(enrollment, tx); err != nil { + return nil, err + } + node, err := s.nodes.GetNodeByID(enrollment.NodeID, tx) + if err != nil { + return nil, err + } + node.CertificateFingerprint, node.Status = issued.Fingerprint, request.RepositoryStorageNodeStatusInstalling + if err = s.nodes.UpdateNode(node, tx); err != nil { + return nil, err + } + if err = s.markInstallTaskRunning(enrollment.InstallTaskID, now, tx); err != nil { + return nil, err + } + return s.certificateBundle(node.ID, certificate), nil +} + +func (s *service) certificateBundle(nodeID string, certificate *po.RepositoryNodeCertificate) *request.RepositoryNodeCertificateBundle { + return &request.RepositoryNodeCertificateBundle{NodeID: nodeID, + CertificatePEM: []byte(certificate.CertificatePEM), CACertificatePEM: []byte(certificate.CertificateChainPEM), + SerialNumber: certificate.SerialNumber, Fingerprint: certificate.Fingerprint, + NotBefore: certificate.NotBefore, NotAfter: certificate.NotAfter} +} + +func (s *service) markBootstrapIssuanceFailed(enrollmentID, leaseOwner string, cause error) { + var err error + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + enrollment, err := s.lifecycle.GetEnrollmentByIDForUpdate(enrollmentID, tx) + if err != nil || enrollment.IssuanceLeaseOwner != leaseOwner { + return + } + enrollment.IssuanceStatus = request.RepositoryNodeIssuanceFailed + enrollment.IssuanceLeaseOwner, enrollment.IssuanceLeaseExpiresAt = "", nil + enrollment.LastErrorCode = "REPOSITORY_NODE_CERTIFICATE_ISSUANCE_FAILED" + enrollment.LastErrorMessage = cause.Error() + _ = s.lifecycle.UpdateEnrollment(enrollment, tx) +} + +func (s *service) RenewNodeCertificate(nodeID string, csrPEM []byte) ( + bundle *request.RepositoryNodeCertificateBundle, err error, +) { + if s.lifecycle == nil || s.issuer == nil { + return nil, ErrNodeLifecycleDisabled + } + if uuid.Validate(nodeID) != nil || len(csrPEM) == 0 || len(csrPEM) > 32<<10 { + return nil, fmt.Errorf("repository node renewal request is invalid") + } + now := s.now() + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + node, err := s.nodes.GetNodeByID(nodeID, tx) + if err != nil || !node.Enabled || node.Status == request.RepositoryStorageNodeStatusDisabled { + return nil, ErrCertificateMismatch + } + current, findErr := s.lifecycle.FindUsableCertificate(nodeID, node.CertificateFingerprint, now, tx) + if findErr == nil && current.NotAfter.Sub(now) > s.lifecyclePolicy.RenewBefore { + return nil, ErrCertificateRenewalTooEarly + } + if findErr != nil && !errors.Is(findErr, repositorystoragenoderepo.ErrNodeCertificateNotFound) { + return nil, findErr + } + issued, err := s.issuer.Issue(csrPEM, nodeID, node.TLSServerName) + if err != nil { + return nil, err + } + if err = s.lifecycle.SupersedeActiveCertificates(nodeID, now.Add(s.lifecyclePolicy.CertificateGrace), tx); err != nil { + return nil, err + } + certificate := &po.RepositoryNodeCertificate{ID: uuid.NewString(), NodeID: nodeID, + SerialNumber: issued.SerialNumber, Fingerprint: issued.Fingerprint, + Status: request.RepositoryNodeCertificateActive, NotBefore: issued.NotBefore, NotAfter: issued.NotAfter} + if err = s.lifecycle.CreateCertificate(certificate, tx); err != nil { + return nil, err + } + node.CertificateFingerprint = issued.Fingerprint + if err = s.nodes.UpdateNode(node, tx); err != nil { + return nil, err + } + return &request.RepositoryNodeCertificateBundle{NodeID: nodeID, CertificatePEM: issued.PEM, + CACertificatePEM: s.issuer.CACertificatePEM(), SerialNumber: issued.SerialNumber, + Fingerprint: issued.Fingerprint, NotBefore: issued.NotBefore, NotAfter: issued.NotAfter}, nil +} + +func (s *service) markInstallTaskRunning(taskID string, now time.Time, session entity.Session) error { + task, err := s.tasks.GetByIDForUpdate(taskID, session) + if err != nil { + return err + } + if task.Status == request.RepositoryTaskStatusPending { + task.Status, task.ProgressPercent, task.CurrentStep = request.RepositoryTaskStatusRunning, 80, + "certificate issued; waiting for first heartbeat" + task.StartedAt = &now + if err = s.tasks.Update(task, session); err != nil { + return err + } + } + targets, err := s.targets.ListTargets(taskID, session) + if err != nil { + return err + } + for index := range targets { + if targets[index].Status == request.RepositoryTaskStatusPending { + targets[index].Status, targets[index].ProgressPercent = request.RepositoryTaskStatusRunning, 80 + targets[index].StartedAt = &now + if err = s.targets.UpdateTarget(&targets[index], session); err != nil { + return err + } + } + } + return nil +} + +func (s *service) completeEnrollmentOnHeartbeat(nodeID string, now time.Time, session entity.Session) error { + if s.lifecycle == nil || s.tasks == nil || s.targets == nil { + return nil + } + enrollment, err := s.lifecycle.FindLatestEnrollmentByNode(nodeID, session) + if errors.Is(err, repositorystoragenoderepo.ErrNodeEnrollmentNotFound) { + return nil + } + if err != nil || (enrollment.Status != request.RepositoryNodeEnrollmentBootstrapped && + enrollment.Status != request.RepositoryNodeEnrollmentRegistered) { + return err + } + enrollment.Status = request.RepositoryNodeEnrollmentOnline + if err = s.lifecycle.UpdateEnrollment(enrollment, session); err != nil { + return err + } + task, err := s.tasks.GetByIDForUpdate(enrollment.InstallTaskID, session) + if err != nil { + return err + } + task.Status, task.ProgressPercent, task.CurrentStep = request.RepositoryTaskStatusSuccess, 100, + "repository node is online" + task.FinishedAt = &now + if err = s.tasks.Update(task, session); err != nil { + return err + } + targets, err := s.targets.ListTargets(task.ID, session) + if err != nil { + return err + } + for index := range targets { + targets[index].Status, targets[index].ProgressPercent = request.RepositoryTaskStatusSuccess, 100 + targets[index].FinishedAt = &now + if err = s.targets.UpdateTarget(&targets[index], session); err != nil { + return err + } + } + return nil +} + +func newBootstrapToken() (plain, hash string, err error) { + raw := make([]byte, 32) + if _, err = rand.Read(raw); err != nil { + return "", "", fmt.Errorf("generate repository node bootstrap token: %w", err) + } + plain = base64.RawURLEncoding.EncodeToString(raw) + digest := sha256.Sum256([]byte(plain)) + return plain, hex.EncodeToString(digest[:]), nil +} + +func (s *service) RegisterArtifact(ctx entity.Context, input request.RepositoryNodeArtifactSpec) ( + value *po.RepositoryNodeArtifact, err error, +) { + if s.lifecycle == nil { + return nil, ErrNodeLifecycleDisabled + } + if s.artifactRoot == "" { + return nil, fmt.Errorf("repository node artifact storage is not configured") + } + if strings.TrimSpace(ctx.User.LoginName) == "" { + return nil, ErrUnauthenticated + } + input.Name, input.Version, input.Release = strings.TrimSpace(input.Name), + strings.TrimSpace(input.Version), strings.TrimSpace(input.Release) + input.Architecture, err = repositoryNodePlatform.NormalizeArchitecture(input.Architecture) + if err != nil { + return nil, fmt.Errorf("repository node artifact architecture is invalid: %w", err) + } + input.FileName, input.SHA256 = strings.TrimSpace(input.FileName), strings.ToLower(strings.TrimSpace(input.SHA256)) + input.BuildCommit, input.RelativePath = strings.TrimSpace(input.BuildCommit), strings.TrimSpace(input.RelativePath) + if input.Name == "" || input.Version == "" || input.Release == "" || input.SizeBytes <= 0 || + !sha256Pattern.MatchString(input.SHA256) || len(input.BuildCommit) < 7 { + return nil, fmt.Errorf("repository node artifact metadata is invalid") + } + cleanPath := path.Clean(input.RelativePath) + if cleanPath == "." || cleanPath == ".." || strings.HasPrefix(cleanPath, "../") || + strings.HasPrefix(cleanPath, "/") || path.Base(cleanPath) != input.FileName { + return nil, fmt.Errorf("repository node artifact relative_path is invalid") + } + artifactPath, err := s.resolveArtifactPath(cleanPath) + if err != nil { + return nil, err + } + actualSize, actualSHA256, err := hashArtifact(artifactPath) + if err != nil { + return nil, err + } + if actualSize != input.SizeBytes || subtle.ConstantTimeCompare([]byte(actualSHA256), []byte(input.SHA256)) != 1 { + return nil, fmt.Errorf("repository node artifact size or SHA-256 does not match the stored file") + } + status := strings.ToUpper(strings.TrimSpace(input.Status)) + if status == "" { + status = request.RepositoryNodeArtifactAvailable + } + if status != request.RepositoryNodeArtifactAvailable && status != request.RepositoryNodeArtifactDisabled { + return nil, fmt.Errorf("repository node artifact status is invalid") + } + value = &po.RepositoryNodeArtifact{ID: uuid.NewString(), Name: input.Name, Version: input.Version, + Release: input.Release, Architecture: input.Architecture, FileName: input.FileName, + SizeBytes: input.SizeBytes, SHA256: input.SHA256, BuildCommit: input.BuildCommit, + RelativePath: cleanPath, Status: status} + if err = s.lifecycle.CreateArtifact(value, nil); err != nil { + return nil, err + } + return value, nil +} + +func (s *service) DownloadArtifact(ctx entity.Context, artifactID string) ( + *request.RepositoryNodeArtifactDownload, error, +) { + if s.lifecycle == nil || s.artifactRoot == "" { + return nil, ErrNodeLifecycleDisabled + } + if strings.TrimSpace(ctx.User.LoginName) == "" { + return nil, ErrUnauthenticated + } + if uuid.Validate(strings.TrimSpace(artifactID)) != nil { + return nil, fmt.Errorf("repository node artifact id must be a UUID") + } + artifact, err := s.lifecycle.GetArtifactByID(artifactID, nil) + if err != nil { + return nil, err + } + if artifact.Status != request.RepositoryNodeArtifactAvailable { + return nil, fmt.Errorf("repository node artifact is not available") + } + artifactPath, err := s.resolveArtifactPath(artifact.RelativePath) + if err != nil { + return nil, err + } + actualSize, actualSHA256, err := hashArtifact(artifactPath) + if err != nil { + return nil, err + } + if actualSize != artifact.SizeBytes || subtle.ConstantTimeCompare([]byte(actualSHA256), []byte(artifact.SHA256)) != 1 { + return nil, fmt.Errorf("repository node artifact integrity verification failed") + } + content, err := os.Open(artifactPath) + if err != nil { + return nil, err + } + return &request.RepositoryNodeArtifactDownload{FileName: artifact.FileName, + ContentType: "application/x-rpm", SizeBytes: actualSize, SHA256: actualSHA256, Content: content}, nil +} + +func (s *service) resolveArtifactPath(relativePath string) (string, error) { + cleanPath := filepath.Clean(filepath.FromSlash(relativePath)) + joined := filepath.Join(s.artifactRoot, cleanPath) + resolved, err := filepath.EvalSymlinks(joined) + if err != nil { + return "", fmt.Errorf("resolve repository node artifact: %w", err) + } + relative, err := filepath.Rel(s.artifactRoot, resolved) + if err != nil || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) { + return "", fmt.Errorf("repository node artifact escapes the configured root") + } + info, err := os.Stat(resolved) + if err != nil || !info.Mode().IsRegular() { + return "", fmt.Errorf("repository node artifact is not a regular file") + } + return resolved, nil +} + +func hashArtifact(filePath string) (int64, string, error) { + file, err := os.Open(filePath) + if err != nil { + return 0, "", fmt.Errorf("open repository node artifact: %w", err) + } + defer file.Close() + hash := sha256.New() + size, err := io.Copy(hash, file) + if err != nil { + return 0, "", fmt.Errorf("hash repository node artifact: %w", err) + } + return size, hex.EncodeToString(hash.Sum(nil)), nil +} + +func (s *service) ListArtifacts(ctx entity.Context, projectID int64, status string) ([]po.RepositoryNodeArtifact, error) { + if s.lifecycle == nil { + return nil, ErrNodeLifecycleDisabled + } + if strings.TrimSpace(ctx.User.LoginName) == "" { + return nil, ErrUnauthenticated + } + if err := s.authorizeProject(ctx, projectID, nil); err != nil { + return nil, err + } + status = strings.ToUpper(strings.TrimSpace(status)) + if status != "" && status != request.RepositoryNodeArtifactAvailable && + status != request.RepositoryNodeArtifactPublished && status != request.RepositoryNodeArtifactDisabled { + return nil, fmt.Errorf("repository node artifact status is invalid") + } + values, err := s.lifecycle.ListArtifacts(status, nil) + if err != nil { + return nil, err + } + filtered := make([]po.RepositoryNodeArtifact, 0, len(values)) + for _, value := range values { + if value.ProjectID == projectID { + filtered = append(filtered, value) + } + } + return filtered, nil +} + +func (s *service) FindAvailableArtifact(architecture string) (*po.RepositoryNodeArtifact, error) { + if s.lifecycle == nil { + return nil, ErrNodeLifecycleDisabled + } + architecture = strings.ToLower(strings.TrimSpace(architecture)) + values, err := s.lifecycle.ListArtifacts(request.RepositoryNodeArtifactAvailable, nil) + if err != nil { + return nil, err + } + for index := range values { + if values[index].Architecture == architecture { + return &values[index], nil + } + } + return nil, repositorystoragenoderepo.ErrNodeArtifactNotFound +} + +func (s *service) MarkEnrollmentInstalling(enrollmentID, tmsTaskID string) (err error) { + if s.lifecycle == nil || s.tasks == nil || s.targets == nil { + return ErrNodeLifecycleDisabled + } + if uuid.Validate(enrollmentID) != nil || strings.TrimSpace(tmsTaskID) == "" { + return fmt.Errorf("repository node installation dispatch is invalid") + } + now := s.now() + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + enrollment, err := s.lifecycle.GetEnrollmentByIDForUpdate(enrollmentID, tx) + if err != nil { + return err + } + if enrollment.Status != request.RepositoryNodeEnrollmentPending { + return fmt.Errorf("repository node enrollment is not pending") + } + enrollment.Status = request.RepositoryNodeEnrollmentInstalling + enrollment.TMSTaskID = strings.TrimSpace(tmsTaskID) + if err = s.lifecycle.UpdateEnrollment(enrollment, tx); err != nil { + return err + } + task, err := s.tasks.GetByIDForUpdate(enrollment.InstallTaskID, tx) + if err != nil { + return err + } + task.Status, task.ProgressPercent = request.RepositoryTaskStatusRunning, 20 + task.CurrentStep, task.StartedAt = "repository node installation dispatched through TMS", &now + if err = s.tasks.Update(task, tx); err != nil { + return err + } + targets, err := s.targets.ListTargets(task.ID, tx) + if err != nil { + return err + } + result, _ := json.Marshal(map[string]string{"tms_task_id": strings.TrimSpace(tmsTaskID)}) + for index := range targets { + targets[index].Status, targets[index].ProgressPercent = request.RepositoryTaskStatusRunning, 20 + targets[index].StartedAt, targets[index].ResultJSON = &now, result + if err = s.targets.UpdateTarget(&targets[index], tx); err != nil { + return err + } + } + return nil +} + +func (s *service) ListActiveInstallations(limit int) ([]po.RepositoryNodeEnrollment, error) { + if s.lifecycle == nil { + return nil, ErrNodeLifecycleDisabled + } + return s.lifecycle.ListActiveInstallations(limit, nil) +} + +func (s *service) FailEnrollment(enrollmentID, errorCode, errorMessage string) (err error) { + return s.finishEnrollment(enrollmentID, request.RepositoryNodeEnrollmentFailed, + "repository node installation failed", errorCode, errorMessage) +} + +// ExpireEnrollment closes an installation that did not bootstrap before its +// one-time enrollment deadline. The associated task still uses FAILED because +// the shared task state machine has no EXPIRED terminal state. +func (s *service) ExpireEnrollment(enrollmentID, errorCode, errorMessage string) (err error) { + return s.finishEnrollment(enrollmentID, request.RepositoryNodeEnrollmentExpired, + "repository node enrollment expired", errorCode, errorMessage) +} + +func (s *service) finishEnrollment(enrollmentID, enrollmentStatus, currentStep, + errorCode, errorMessage string, +) (err error) { + if s.lifecycle == nil || s.tasks == nil || s.targets == nil { + return ErrNodeLifecycleDisabled + } + errorCode = strings.TrimSpace(errorCode) + errorMessage = strings.TrimSpace(errorMessage) + if uuid.Validate(enrollmentID) != nil || errorCode == "" || + (enrollmentStatus != request.RepositoryNodeEnrollmentFailed && + enrollmentStatus != request.RepositoryNodeEnrollmentExpired) { + return fmt.Errorf("repository node installation failure is invalid") + } + if len(errorCode) > 64 { + errorCode = errorCode[:64] + } + if len(errorMessage) > 2048 { + errorMessage = errorMessage[:2048] + } + now := s.now() + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + enrollment, err := s.lifecycle.GetEnrollmentByIDForUpdate(enrollmentID, tx) + if err != nil { + return err + } + if enrollment.Status == request.RepositoryNodeEnrollmentOnline { + return fmt.Errorf("online repository node enrollment cannot be failed") + } + enrollment.Status, enrollment.LastErrorCode, enrollment.LastErrorMessage = + enrollmentStatus, errorCode, errorMessage + if err = s.lifecycle.UpdateEnrollment(enrollment, tx); err != nil { + return err + } + if err = s.lifecycle.InvalidateBootstrapTokens(enrollment.ID, now, tx); err != nil { + return err + } + node, err := s.nodes.GetNodeByID(enrollment.NodeID, tx) + if err != nil { + return err + } + preserveOnlineIdentity := false + if node.Status == request.RepositoryStorageNodeStatusOnline && + strings.TrimSpace(node.CertificateFingerprint) != "" { + _, certificateErr := s.lifecycle.FindUsableCertificate(node.ID, node.CertificateFingerprint, now, tx) + preserveOnlineIdentity = certificateErr == nil + } + if !preserveOnlineIdentity { + node.Status = request.RepositoryStorageNodeStatusInstallFailed + } + node.LastErrorCode, node.LastErrorMessage = errorCode, errorMessage + if err = s.nodes.UpdateNode(node, tx); err != nil { + return err + } + task, err := s.tasks.GetByIDForUpdate(enrollment.InstallTaskID, tx) + if err != nil { + return err + } + task.Status, task.ErrorCode, task.ErrorMessage = request.RepositoryTaskStatusFailed, errorCode, errorMessage + task.CurrentStep, task.FinishedAt = currentStep, &now + if err = s.tasks.Update(task, tx); err != nil { + return err + } + targets, err := s.targets.ListTargets(task.ID, tx) + if err != nil { + return err + } + for index := range targets { + targets[index].Status, targets[index].ErrorCode, targets[index].ErrorMessage = + request.RepositoryTaskStatusFailed, errorCode, errorMessage + targets[index].FinishedAt = &now + if err = s.targets.UpdateTarget(&targets[index], tx); err != nil { + return err + } + } + return nil +} diff --git a/manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_mysql_authorization_test.go b/manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_mysql_authorization_test.go new file mode 100644 index 0000000000000000000000000000000000000000..11df60a161e4e9cbda379d7bc487bceebbf9121d --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_mysql_authorization_test.go @@ -0,0 +1,202 @@ +//go:build integration + +package repositoryNodeService + +import ( + "crypto/sha256" + "encoding/hex" + "errors" + "os" + "strings" + "testing" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + mysqlDriver "github.com/go-sql-driver/mysql" + "github.com/google/uuid" +) + +// This test deliberately requires a disposable schema, never the live footstone DB. +func TestMySQLBootstrapAuthorizationAndRevocation(t *testing.T) { + dsn := os.Getenv("OCM_REPOSITORY_TEST_DSN") + if dsn == "" { + t.Skip("OCM_REPOSITORY_TEST_DSN is required") + } + nativeDSN := strings.TrimPrefix(dsn, "mysql://") + cfg, err := mysqlDriver.ParseDSN(nativeDSN) + if err != nil { + t.Fatal("invalid MySQL test DSN") + } + if !strings.HasPrefix(cfg.DBName, "ocm_test_") || os.Getenv("OCM_REPOSITORY_TEST_ALLOW_SCHEMA") != "1" { + t.Fatal("requires disposable ocm_test_ schema and OCM_REPOSITORY_TEST_ALLOW_SCHEMA=1") + } + db, err := database.NewDBClient(config.AccessPoint{Source: "mysql://" + nativeDSN}) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { db.Close() }) + s := New(db) + if err = s.SyncTables(); err != nil { + t.Fatal(err) + } + if err = db.DB().AutoMigrate(&po.RepositoryTask{}); err != nil { + t.Fatal(err) + } + now := time.Now().UTC().Truncate(time.Millisecond) + nodeID, enrollmentID, taskID, allowID := uuid.NewString(), uuid.NewString(), uuid.NewString(), uuid.NewString() + projectID, agentID := int64(921024), uint64(time.Now().UnixNano()) + token := strings.ReplaceAll(uuid.NewString()+uuid.NewString(), "-", "") + digest := sha256.Sum256([]byte(token)) + input := request.RepositoryNodeBootstrapRequest{EnrollmentID: enrollmentID, TaskID: taskID, + TMSAgentID: agentID, AuthorizationID: uuid.NewString(), Architecture: "x86_64", Token: token, CSRPEM: []byte("mysql-test-csr")} + rows := []interface{}{ + &po.RepositoryStorageNode{ID: nodeID, ProjectID: projectID, Name: "mysql-race-" + nodeID, + ControlBaseURL: "https://" + nodeID + ".invalid", PublishBaseURL: "http://" + nodeID + ".invalid", + Status: request.RepositoryStorageNodeStatusPending, Enabled: true, CreatedBy: "mysql-race"}, + &po.RepositoryNodeRegistrationAllowlist{ID: allowID, ProjectID: projectID, TMSAgentID: agentID, + ExpectedArchitecture: "x86_64", Status: request.RepositoryNodeAllowlistEnabled, CreatedBy: "mysql-race", UpdatedBy: "mysql-race"}, + &po.RepositoryTask{ID: taskID, RootTaskID: taskID, ProjectID: projectID, + Type: request.RepositoryTaskTypeNodeInstall, Status: request.RepositoryTaskStatusPending, RequestedBy: "mysql-race", AvailableAt: now, PayloadJSON: "{}"}, + &po.RepositoryNodeEnrollment{ID: enrollmentID, NodeID: nodeID, ProjectID: projectID, TMSAgentID: agentID, + Generation: 1, Status: request.RepositoryNodeEnrollmentInstalling, InstallTaskID: taskID, + AllowlistEntryID: allowID, ExpectedArchitecture: "x86_64", ExpiresAt: now.Add(time.Hour), CreatedBy: "mysql-race"}, + &po.RepositoryNodeBootstrapToken{ID: uuid.NewString(), EnrollmentID: enrollmentID, TaskID: taskID, + Generation: 1, TMSAgentID: agentID, TokenHash: hex.EncodeToString(digest[:]), ExpiresAt: now.Add(time.Hour)}, + } + for _, row := range rows { + if err = db.DB().Create(row).Error; err != nil { + t.Fatal(err) + } + } + t.Cleanup(func() { + for _, model := range []interface{}{&po.RepositoryNodeCertificate{}, &po.RepositoryNodeBootstrapToken{}} { + if e := db.DB().Where("enrollment_id = ?", enrollmentID).Delete(model).Error; e != nil { + t.Error(e) + } + } + for _, row := range rows[:4] { + if e := db.DB().Delete(row).Error; e != nil { + t.Error(e) + } + } + }) + issuer := &mysqlBlockingIssuer{entered: make(chan struct{}), release: make(chan struct{})} + s.ConfigureLifecycle(issuer, request.RepositoryNodeLifecyclePolicy{}) + + t.Run("disable commits first rejects without consuming token", func(t *testing.T) { + tx := db.DB().Begin() + defer tx.Rollback() + if e := tx.Exec("SELECT id FROM repository_node_registration_allowlists WHERE id = ? FOR UPDATE", allowID).Error; e != nil { + t.Fatal(e) + } + if e := tx.Model(&po.RepositoryNodeRegistrationAllowlist{}).Where("id = ?", allowID).Update("status", "DISABLED").Error; e != nil { + t.Fatal(e) + } + result := make(chan error, 1) + go func() { _, e := s.authorizeBootstrap(input); result <- e }() + select { + case e := <-result: + t.Fatalf("authorization did not wait for allowlist transaction: %v", e) + case <-time.After(300 * time.Millisecond): + } + if e := tx.Commit().Error; e != nil { + t.Fatal(e) + } + select { + case e := <-result: + if !errors.Is(e, ErrNodeRegistrationDenied) { + t.Fatalf("expected registration denial: %v", e) + } + case <-time.After(10 * time.Second): + t.Fatal("authorization blocked") + } + var stored po.RepositoryNodeBootstrapToken + if e := db.DB().Where("enrollment_id = ?", enrollmentID).First(&stored).Error; e != nil { + t.Fatal(e) + } + if stored.ConsumedAt != nil { + t.Fatal("denied authorization consumed token") + } + }) + if err = db.DB().Model(&po.RepositoryNodeRegistrationAllowlist{}).Where("id = ?", allowID).Update("status", "ENABLED").Error; err != nil { + t.Fatal(err) + } + t.Run("authorization commits first issuance outside lock and exact recovery", func(t *testing.T) { + type response struct { + bundle *request.RepositoryNodeCertificateBundle + err error + } + result := make(chan response, 1) + go func() { bundle, e := s.BootstrapNode(input); result <- response{bundle, e} }() + select { + case <-issuer.entered: + case <-time.After(10 * time.Second): + t.Fatal("issuer not reached") + } + // A separate transaction can acquire the same row while issuance is blocked. + tx := db.DB().Begin() + if e := tx.Exec("SET innodb_lock_wait_timeout = 2").Error; e != nil { + t.Fatal(e) + } + if e := tx.Exec("SELECT id FROM repository_node_registration_allowlists WHERE id = ? FOR UPDATE", allowID).Error; e != nil { + tx.Rollback() + t.Fatal(e) + } + if e := tx.Model(&po.RepositoryNodeRegistrationAllowlist{}).Where("id = ?", allowID).Update("status", "DISABLED").Error; e != nil { + tx.Rollback() + t.Fatal(e) + } + if e := tx.Commit().Error; e != nil { + t.Fatal(e) + } + close(issuer.release) + var first response + select { + case first = <-result: + case <-time.After(10 * time.Second): + t.Fatal("issuance completion blocked") + } + if first.err != nil { + t.Fatal(first.err) + } + if e := db.DB().Model(&po.RepositoryTask{}).Where("id = ?", taskID).Update("status", request.RepositoryTaskStatusSuccess).Error; e != nil { + t.Fatal(e) + } + recovered, e := s.BootstrapNode(input) + if e != nil || string(recovered.CertificatePEM) != string(first.bundle.CertificatePEM) { + t.Fatalf("persisted recovery failed: %v", e) + } + for _, change := range []string{"csr", "authorization", "agent", "architecture"} { + bad := input + switch change { + case "csr": + bad.CSRPEM = []byte("different-csr") + case "authorization": + bad.AuthorizationID = uuid.NewString() + case "agent": + bad.TMSAgentID++ + case "architecture": + bad.Architecture = "aarch64" + } + if _, e = s.BootstrapNode(bad); e == nil { + t.Fatalf("%s replay accepted", change) + } + } + var count int64 + if e := db.DB().Model(&po.RepositoryNodeCertificate{}).Where("enrollment_id = ?", enrollmentID).Count(&count).Error; e != nil || count != 1 { + t.Fatalf("certificate count=%d error=%v", count, e) + } + }) +} + +type mysqlBlockingIssuer struct{ entered, release chan struct{} } + +func (i *mysqlBlockingIssuer) Issue(csr []byte, nodeID, name string) (request.RepositoryIssuedCertificate, error) { + close(i.entered) + <-i.release + return (fakeCertificateIssuer{}).Issue(csr, nodeID, name) +} +func (*mysqlBlockingIssuer) CACertificatePEM() []byte { return []byte("test-ca") } diff --git a/manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_test.go b/manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_test.go new file mode 100644 index 0000000000000000000000000000000000000000..92a316612f0f24d7c7179e445524ea4e02c57733 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryNodeService/lifecycle_test.go @@ -0,0 +1,802 @@ +package repositoryNodeService + +import ( + "crypto/sha256" + "encoding/hex" + "errors" + "io" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorystoragenoderepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorytaskrepo" +) + +func TestEnrollmentBootstrapTokenIsOneTimeAndHeartbeatCompletesInstall(t *testing.T) { + nodes := &fakeNodeRepository{} + service, mock, sqlDB := newNodeTestService(t, nodes) + defer sqlDB.Close() + now := time.Date(2026, 9, 20, 13, 0, 0, 0, time.UTC) + service.now = func() time.Time { return now } + lifecycle := &fakeLifecycleRepository{allowlist: &po.RepositoryNodeRegistrationAllowlist{ + ID: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", ProjectID: 42, TMSAgentID: 991, + ExpectedArchitecture: "x86_64", Status: request.RepositoryNodeAllowlistEnabled, + }} + tasks := &fakeLifecycleTaskRepository{} + targets := &fakeLifecycleTargetRepository{} + service.lifecycle, service.tasks, service.targets = lifecycle, tasks, targets + service.ConfigureLifecycle(fakeCertificateIssuer{}, request.RepositoryNodeLifecyclePolicy{ + EnrollmentTTL: time.Minute, CertificateTTL: 90 * 24 * time.Hour, + RenewBefore: 30 * 24 * time.Hour, CertificateGrace: 24 * time.Hour, + }) + + mock.ExpectBegin() + mock.ExpectCommit() + result, err := service.CreateEnrollment(globalNodeContext(), request.RepositoryNodeEnrollmentSpec{ + Node: validNodeSpec(), TMSAgentID: 991, ExpectedArchitecture: "x86_64", + }) + if err != nil { + t.Fatal(err) + } + if lifecycle.token == nil || lifecycle.token.TokenHash == "" || + strings.Contains(lifecycle.token.TokenHash, result.BootstrapToken) || len(lifecycle.token.TokenHash) != 64 { + t.Fatalf("stored bootstrap token = %#v", lifecycle.token) + } + if result.Node.Status != request.RepositoryStorageNodeStatusPending || + result.Task.Status != request.RepositoryTaskStatusPending || result.Target.TMSAgentID != 991 { + t.Fatalf("enrollment result = %#v", result) + } + lifecycle.enrollment.Status = request.RepositoryNodeEnrollmentInstalling + + mock.ExpectBegin() + mock.ExpectCommit() + mock.ExpectBegin() + mock.ExpectCommit() + authorizationID := "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" + bundle, err := service.BootstrapNode(request.RepositoryNodeBootstrapRequest{ + EnrollmentID: result.Enrollment.ID, TaskID: result.Task.ID, TMSAgentID: 991, + AuthorizationID: authorizationID, Architecture: "x86_64", + Token: result.BootstrapToken, CSRPEM: []byte("test-csr"), + }) + if err != nil { + t.Fatal(err) + } + if lifecycle.token.ConsumedAt == nil || lifecycle.enrollment.Status != request.RepositoryNodeEnrollmentRegistered || + nodes.node.Status != request.RepositoryStorageNodeStatusInstalling || + tasks.task.Status != request.RepositoryTaskStatusRunning || targets.target.Status != request.RepositoryTaskStatusRunning || + bundle.NodeID != nodes.node.ID { + t.Fatalf("bootstrap state: token=%#v enrollment=%#v node=%#v task=%#v target=%#v bundle=%#v", + lifecycle.token, lifecycle.enrollment, nodes.node, tasks.task, targets.target, bundle) + } + + mock.ExpectBegin() + mock.ExpectCommit() + recovered, err := service.BootstrapNode(request.RepositoryNodeBootstrapRequest{ + EnrollmentID: result.Enrollment.ID, TaskID: result.Task.ID, TMSAgentID: 991, + AuthorizationID: authorizationID, Architecture: "x86_64", + Token: result.BootstrapToken, CSRPEM: []byte("test-csr"), + }) + if err != nil || string(recovered.CertificatePEM) != string(bundle.CertificatePEM) { + t.Fatalf("exact BootstrapNode() retry bundle=%#v error=%v", recovered, err) + } + + mock.ExpectBegin() + mock.ExpectRollback() + if _, err = service.BootstrapNode(request.RepositoryNodeBootstrapRequest{ + EnrollmentID: result.Enrollment.ID, TaskID: result.Task.ID, TMSAgentID: 991, + AuthorizationID: authorizationID, Architecture: "x86_64", + Token: result.BootstrapToken, CSRPEM: []byte("different-csr"), + }); !errors.Is(err, ErrBootstrapTokenReplay) { + t.Fatalf("generic replay BootstrapNode() error = %v", err) + } + + mock.ExpectBegin() + mock.ExpectRollback() + if _, err = service.BootstrapNode(request.RepositoryNodeBootstrapRequest{ + EnrollmentID: result.Enrollment.ID, TaskID: result.Task.ID, TMSAgentID: 992, + AuthorizationID: authorizationID, Architecture: "x86_64", + Token: result.BootstrapToken, CSRPEM: []byte("test-csr"), + }); !errors.Is(err, ErrBootstrapUnauthorized) { + t.Fatalf("different agent BootstrapNode() error = %v", err) + } + + // Revocation controls new authorizations, not recovery of an authorization + // already committed before revocation (even if its install task finished). + lifecycle.allowlist.Status = request.RepositoryNodeAllowlistDisabled + tasks.task.Status = request.RepositoryTaskStatusSuccess + mock.ExpectBegin() + mock.ExpectCommit() + recovered, err = service.BootstrapNode(request.RepositoryNodeBootstrapRequest{ + EnrollmentID: result.Enrollment.ID, TaskID: result.Task.ID, TMSAgentID: 991, + AuthorizationID: authorizationID, Architecture: "x86_64", + Token: result.BootstrapToken, CSRPEM: []byte("test-csr"), + }) + if err != nil || string(recovered.CertificatePEM) != string(bundle.CertificatePEM) { + t.Fatalf("persisted authorization recovery after revoke/task completion: error=%v", err) + } + + now = now.Add(time.Minute) + mock.ExpectBegin() + mock.ExpectCommit() + if _, err = service.RecordHeartbeat(request.RepositoryStorageNodeHeartbeat{ + NodeID: nodes.node.ID, Architecture: "x86_64", OSName: "OpenCloudOS", OSVersion: "9.6", + AgentVersion: "1.0.0", FilesystemType: "xfs", CapacityBytes: 1 << 30, + FreeBytes: 1 << 29, FreeInodes: 20000, ToolVersions: requiredNodeTools(), + }); err != nil { + t.Fatal(err) + } + if lifecycle.enrollment.Status != request.RepositoryNodeEnrollmentOnline || + tasks.task.Status != request.RepositoryTaskStatusSuccess || tasks.task.ProgressPercent != 100 || + targets.target.Status != request.RepositoryTaskStatusSuccess || targets.target.ProgressPercent != 100 || + nodes.node.Status != request.RepositoryStorageNodeStatusOnline || nodes.node.AgentVersion != "1.0.0" { + t.Fatalf("heartbeat completion: enrollment=%#v node=%#v task=%#v target=%#v", + lifecycle.enrollment, nodes.node, tasks.task, targets.target) + } + if err = mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } +} + +func TestFailedEnrollmentReusesStableNodeIdentity(t *testing.T) { + existingNode := &po.RepositoryStorageNode{ID: testNodeID, ProjectID: 42, Name: "old-node", + Enabled: true, Status: request.RepositoryStorageNodeStatusInstallFailed, + CertificateFingerprint: strings.Repeat("cd", 32), CreatedBy: "admin"} + nodes := &fakeNodeRepository{node: existingNode} + service, mock, sqlDB := newNodeTestService(t, nodes) + defer sqlDB.Close() + lifecycle := &fakeLifecycleRepository{enrollment: &po.RepositoryNodeEnrollment{ + ID: "77777777-7777-4777-8777-777777777777", NodeID: testNodeID, ProjectID: 42, + TMSAgentID: 991, Generation: 2, Status: request.RepositoryNodeEnrollmentFailed, + }, allowlist: &po.RepositoryNodeRegistrationAllowlist{ + ID: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", ProjectID: 42, TMSAgentID: 991, + ExpectedArchitecture: "aarch64", Status: request.RepositoryNodeAllowlistEnabled, + }, certs: []po.RepositoryNodeCertificate{{NodeID: testNodeID, + Fingerprint: strings.Repeat("cd", 32), Status: request.RepositoryNodeCertificateActive}}} + service.lifecycle = lifecycle + service.tasks = &fakeLifecycleTaskRepository{} + service.targets = &fakeLifecycleTargetRepository{} + service.ConfigureLifecycle(fakeCertificateIssuer{}, request.RepositoryNodeLifecyclePolicy{EnrollmentTTL: time.Minute}) + + mock.ExpectBegin() + mock.ExpectCommit() + result, err := service.CreateEnrollment(globalNodeContext(), request.RepositoryNodeEnrollmentSpec{ + Node: validNodeSpec(), TMSAgentID: 991, ExpectedArchitecture: "aarch64", + }) + if err != nil { + t.Fatal(err) + } + if result.Node.ID != testNodeID || result.Enrollment.Generation != 3 || + result.Enrollment.NodeID != testNodeID || nodes.node.CertificateFingerprint != "" || + nodes.node.Status != request.RepositoryStorageNodeStatusPending || nodes.node.Architecture != "aarch64" { + t.Fatalf("reinstall result = %#v, node = %#v", result, nodes.node) + } + if lifecycle.invalidatedEnrollmentID != "77777777-7777-4777-8777-777777777777" || + lifecycle.certs[0].Status != request.RepositoryNodeCertificateActive { + t.Fatalf("previous certificate should remain until replacement issuance: lifecycle = %#v", lifecycle) + } + if err = mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } +} + +func TestOnlineEnrollmentReinstallReusesStableNodeIdentity(t *testing.T) { + existingNode := &po.RepositoryStorageNode{ID: testNodeID, ProjectID: 42, Name: "online-node", + Enabled: true, Status: request.RepositoryStorageNodeStatusOnline, + CertificateFingerprint: strings.Repeat("ef", 32), CreatedBy: "admin"} + nodes := &fakeNodeRepository{node: existingNode} + service, mock, sqlDB := newNodeTestService(t, nodes) + defer sqlDB.Close() + lifecycle := &fakeLifecycleRepository{enrollment: &po.RepositoryNodeEnrollment{ + ID: "88888888-8888-4888-8888-888888888888", NodeID: testNodeID, ProjectID: 42, + TMSAgentID: 991, Generation: 3, Status: request.RepositoryNodeEnrollmentOnline, + }, allowlist: &po.RepositoryNodeRegistrationAllowlist{ + ID: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", ProjectID: 42, TMSAgentID: 991, + ExpectedArchitecture: "x86_64", Status: request.RepositoryNodeAllowlistEnabled, + }, certs: []po.RepositoryNodeCertificate{{NodeID: testNodeID, + Fingerprint: strings.Repeat("ef", 32), Status: request.RepositoryNodeCertificateActive}}} + service.lifecycle = lifecycle + tasks := &fakeLifecycleTaskRepository{} + targets := &fakeLifecycleTargetRepository{} + service.tasks = tasks + service.targets = targets + service.ConfigureLifecycle(fakeCertificateIssuer{}, request.RepositoryNodeLifecyclePolicy{EnrollmentTTL: time.Minute}) + + mock.ExpectBegin() + mock.ExpectCommit() + result, err := service.CreateEnrollment(globalNodeContext(), request.RepositoryNodeEnrollmentSpec{ + Node: validNodeSpec(), TMSAgentID: 991, ExpectedArchitecture: "x86_64", + }) + if err != nil { + t.Fatal(err) + } + if result.Node.ID != testNodeID || result.Enrollment.Generation != 4 || + result.Enrollment.NodeID != testNodeID || nodes.node.CertificateFingerprint != strings.Repeat("ef", 32) || + nodes.node.Status != request.RepositoryStorageNodeStatusOnline { + t.Fatalf("online reinstall result = %#v, node = %#v", result, nodes.node) + } + if lifecycle.invalidatedEnrollmentID != "88888888-8888-4888-8888-888888888888" || + lifecycle.certs[0].Status != request.RepositoryNodeCertificateActive { + t.Fatalf("online identity was not preserved until certificate issuance: lifecycle = %#v", lifecycle) + } + // A failed in-place upgrade must fail only the new enrollment/task. The + // existing node remains ONLINE while its previous certificate is usable. + lifecycle.certs[0].NotBefore = time.Now().UTC().Add(-time.Hour) + lifecycle.certs[0].NotAfter = time.Now().UTC().Add(time.Hour) + mock.ExpectBegin() + mock.ExpectCommit() + if err = service.FailEnrollment(result.Enrollment.ID, "REPOSITORY_NODE_TMS_INSTALL_FAILED", "download failed"); err != nil { + t.Fatal(err) + } + if nodes.node.Status != request.RepositoryStorageNodeStatusOnline || + nodes.node.CertificateFingerprint != strings.Repeat("ef", 32) || + tasks.task.Status != request.RepositoryTaskStatusFailed || + targets.target.Status != request.RepositoryTaskStatusFailed { + t.Fatalf("failed online upgrade disrupted active node: node=%#v task=%#v target=%#v", + nodes.node, tasks.task, targets.target) + } + if err = mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } +} + +func TestLegacyOnlineEnrollmentBindsTMSAgentWithoutCreatingSecondNode(t *testing.T) { + existingNode := &po.RepositoryStorageNode{ + ID: testNodeID, ProjectID: 42, Name: "repo-node-a", Architecture: "x86_64", + Enabled: true, Status: request.RepositoryStorageNodeStatusOnline, + CertificateFingerprint: strings.Repeat("ef", 32), CreatedBy: "admin", + } + nodes := &fakeNodeRepository{node: existingNode} + service, mock, sqlDB := newNodeTestService(t, nodes) + defer sqlDB.Close() + lifecycle := &fakeLifecycleRepository{enrollment: &po.RepositoryNodeEnrollment{ + ID: "99999999-9999-4999-8999-999999999999", NodeID: testNodeID, ProjectID: 42, + TMSAgentID: 0, Generation: 1, Status: request.RepositoryNodeEnrollmentOnline, + }, allowlist: &po.RepositoryNodeRegistrationAllowlist{ + ID: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", ProjectID: 42, TMSAgentID: 991, + ExpectedArchitecture: "x86_64", Status: request.RepositoryNodeAllowlistEnabled, + }} + service.lifecycle = lifecycle + service.tasks = &fakeLifecycleTaskRepository{} + service.targets = &fakeLifecycleTargetRepository{} + service.ConfigureLifecycle(fakeCertificateIssuer{}, request.RepositoryNodeLifecyclePolicy{EnrollmentTTL: time.Minute}) + + mock.ExpectBegin() + mock.ExpectCommit() + result, err := service.CreateEnrollment(globalNodeContext(), request.RepositoryNodeEnrollmentSpec{ + Node: validNodeSpec(), TMSAgentID: 991, ExpectedArchitecture: "x86_64", + }) + if err != nil { + t.Fatal(err) + } + if result.Node.ID != testNodeID || result.Enrollment.NodeID != testNodeID || + result.Enrollment.TMSAgentID != 991 || result.Enrollment.Generation != 2 || + nodes.node.Status != request.RepositoryStorageNodeStatusOnline { + t.Fatalf("legacy binding result=%#v node=%#v", result, nodes.node) + } + if lifecycle.invalidatedEnrollmentID != "99999999-9999-4999-8999-999999999999" { + t.Fatalf("legacy enrollment was not invalidated: %#v", lifecycle) + } + if err = mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } +} + +func TestListArtifactsFiltersByAuthorizedProject(t *testing.T) { + service, _, sqlDB := newNodeTestService(t, &fakeNodeRepository{}) + defer sqlDB.Close() + service.lifecycle = &fakeLifecycleRepository{artifact: &po.RepositoryNodeArtifact{ + ID: "33333333-3333-4333-8333-333333333333", + ProjectID: 42, + Status: request.RepositoryNodeArtifactPublished, + }} + + values, err := service.ListArtifacts(globalNodeContext(), 42, request.RepositoryNodeArtifactPublished) + if err != nil { + t.Fatal(err) + } + if len(values) != 1 || values[0].ProjectID != 42 { + t.Fatalf("project 42 artifacts = %#v", values) + } + + values, err = service.ListArtifacts(globalNodeContext(), 43, request.RepositoryNodeArtifactPublished) + if err != nil { + t.Fatal(err) + } + if len(values) != 0 { + t.Fatalf("cross-project artifacts leaked = %#v", values) + } +} + +func TestBootstrapRechecksAllowlistAfterTokenIssuance(t *testing.T) { + nodes := &fakeNodeRepository{} + service, mock, sqlDB := newNodeTestService(t, nodes) + defer sqlDB.Close() + now := time.Date(2026, 9, 20, 13, 0, 0, 0, time.UTC) + service.now = func() time.Time { return now } + lifecycle := &fakeLifecycleRepository{allowlist: &po.RepositoryNodeRegistrationAllowlist{ + ID: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", ProjectID: 42, TMSAgentID: 991, + ExpectedArchitecture: "x86_64", Status: request.RepositoryNodeAllowlistEnabled, + }} + service.lifecycle = lifecycle + service.tasks = &fakeLifecycleTaskRepository{} + service.targets = &fakeLifecycleTargetRepository{} + service.ConfigureLifecycle(fakeCertificateIssuer{}, request.RepositoryNodeLifecyclePolicy{EnrollmentTTL: time.Minute}) + + mock.ExpectBegin() + mock.ExpectCommit() + result, err := service.CreateEnrollment(globalNodeContext(), request.RepositoryNodeEnrollmentSpec{ + Node: validNodeSpec(), TMSAgentID: 991, ExpectedArchitecture: "x86_64", + }) + if err != nil { + t.Fatal(err) + } + lifecycle.enrollment.Status = request.RepositoryNodeEnrollmentInstalling + lifecycle.allowlist.Status = request.RepositoryNodeAllowlistDisabled + mock.ExpectBegin() + mock.ExpectRollback() + _, err = service.BootstrapNode(request.RepositoryNodeBootstrapRequest{ + EnrollmentID: result.Enrollment.ID, TaskID: result.Task.ID, TMSAgentID: 991, + AuthorizationID: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", Architecture: "x86_64", + Token: result.BootstrapToken, CSRPEM: []byte("test-csr"), + }) + if !errors.Is(err, ErrNodeRegistrationDenied) || lifecycle.token.ConsumedAt != nil { + t.Fatalf("revoked allowlist bootstrap error=%v token=%#v", err, lifecycle.token) + } + if err = mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } +} + +func TestEnrollmentTMSDispatchAndFailureUpdateTaskTargetAndInvalidateToken(t *testing.T) { + const enrollmentID = "88888888-8888-4888-8888-888888888888" + const taskID = "99999999-9999-4999-8999-999999999999" + nodes := &fakeNodeRepository{node: &po.RepositoryStorageNode{ + ID: testNodeID, ProjectID: 42, Name: "pending-node", Enabled: true, + Status: request.RepositoryStorageNodeStatusPending, + }} + service, mock, sqlDB := newNodeTestService(t, nodes) + defer sqlDB.Close() + now := time.Date(2026, 9, 20, 14, 0, 0, 0, time.UTC) + service.now = func() time.Time { return now } + lifecycle := &fakeLifecycleRepository{ + enrollment: &po.RepositoryNodeEnrollment{ + ID: enrollmentID, NodeID: testNodeID, ProjectID: 42, TMSAgentID: 991, + Generation: 1, Status: request.RepositoryNodeEnrollmentPending, InstallTaskID: taskID, + }, + token: &po.RepositoryNodeBootstrapToken{ID: "token", EnrollmentID: enrollmentID}, + } + tasks := &fakeLifecycleTaskRepository{task: &po.RepositoryTask{ + ID: taskID, ProjectID: 42, Status: request.RepositoryTaskStatusPending, + }} + targets := &fakeLifecycleTargetRepository{target: &po.RepositoryTaskTarget{ + ID: "target", TaskID: taskID, TargetType: "TMS_AGENT", + TargetID: testNodeID, StorageNodeID: testNodeID, Status: request.RepositoryTaskStatusPending, + }} + service.lifecycle, service.tasks, service.targets = lifecycle, tasks, targets + + mock.ExpectBegin() + mock.ExpectCommit() + if err := service.MarkEnrollmentInstalling(enrollmentID, "tms-task-123"); err != nil { + t.Fatal(err) + } + if lifecycle.enrollment.Status != request.RepositoryNodeEnrollmentInstalling || + lifecycle.enrollment.TMSTaskID != "tms-task-123" || + tasks.task.Status != request.RepositoryTaskStatusRunning || tasks.task.ProgressPercent != 20 || + targets.target.Status != request.RepositoryTaskStatusRunning || targets.target.ProgressPercent != 20 || + !strings.Contains(string(targets.target.ResultJSON), "tms-task-123") { + t.Fatalf("dispatch state: enrollment=%#v task=%#v target=%#v", + lifecycle.enrollment, tasks.task, targets.target) + } + + now = now.Add(time.Minute) + mock.ExpectBegin() + mock.ExpectCommit() + if err := service.FailEnrollment(enrollmentID, "REPOSITORY_NODE_INSTALL_FAILED", "rpm install failed"); err != nil { + t.Fatal(err) + } + if lifecycle.enrollment.Status != request.RepositoryNodeEnrollmentFailed || + nodes.node.Status != request.RepositoryStorageNodeStatusInstallFailed || + tasks.task.Status != request.RepositoryTaskStatusFailed || tasks.task.FinishedAt == nil || + targets.target.Status != request.RepositoryTaskStatusFailed || targets.target.FinishedAt == nil || + lifecycle.invalidatedEnrollmentID != enrollmentID { + t.Fatalf("failure state: enrollment=%#v node=%#v task=%#v target=%#v lifecycle=%#v", + lifecycle.enrollment, nodes.node, tasks.task, targets.target, lifecycle) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } +} + +func TestExpiredPendingEnrollmentClosesTaskAndInvalidatesToken(t *testing.T) { + const enrollmentID = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" + const taskID = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" + nodes := &fakeNodeRepository{node: &po.RepositoryStorageNode{ + ID: testNodeID, ProjectID: 42, Name: "expired-pending-node", Enabled: true, + Status: request.RepositoryStorageNodeStatusPending, + }} + service, mock, sqlDB := newNodeTestService(t, nodes) + defer sqlDB.Close() + now := time.Date(2026, 9, 29, 9, 0, 0, 0, time.UTC) + service.now = func() time.Time { return now } + lifecycle := &fakeLifecycleRepository{ + enrollment: &po.RepositoryNodeEnrollment{ + ID: enrollmentID, NodeID: testNodeID, ProjectID: 42, TMSAgentID: 991, + Generation: 1, Status: request.RepositoryNodeEnrollmentPending, InstallTaskID: taskID, + ExpiresAt: now.Add(-time.Minute), + }, + token: &po.RepositoryNodeBootstrapToken{ID: "token", EnrollmentID: enrollmentID}, + } + tasks := &fakeLifecycleTaskRepository{task: &po.RepositoryTask{ + ID: taskID, ProjectID: 42, Status: request.RepositoryTaskStatusPending, + }} + targets := &fakeLifecycleTargetRepository{target: &po.RepositoryTaskTarget{ + ID: "target", TaskID: taskID, TargetType: "TMS_AGENT", + TargetID: testNodeID, StorageNodeID: testNodeID, Status: request.RepositoryTaskStatusPending, + }} + service.lifecycle, service.tasks, service.targets = lifecycle, tasks, targets + + mock.ExpectBegin() + mock.ExpectCommit() + if err := service.ExpireEnrollment(enrollmentID, "REPOSITORY_NODE_BOOTSTRAP_TIMEOUT", + "repository node did not complete bootstrap before the enrollment expired"); err != nil { + t.Fatal(err) + } + if lifecycle.enrollment.Status != request.RepositoryNodeEnrollmentExpired || + nodes.node.Status != request.RepositoryStorageNodeStatusInstallFailed || + tasks.task.Status != request.RepositoryTaskStatusFailed || + tasks.task.CurrentStep != "repository node enrollment expired" || tasks.task.FinishedAt == nil || + targets.target.Status != request.RepositoryTaskStatusFailed || targets.target.FinishedAt == nil || + lifecycle.invalidatedEnrollmentID != enrollmentID { + t.Fatalf("expiration state: enrollment=%#v node=%#v task=%#v target=%#v lifecycle=%#v", + lifecycle.enrollment, nodes.node, tasks.task, targets.target, lifecycle) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } +} + +func TestArtifactRegistrationAndDownloadVerifyStoredRPM(t *testing.T) { + service, _, sqlDB := newNodeTestService(t, &fakeNodeRepository{}) + defer sqlDB.Close() + lifecycle := &fakeLifecycleRepository{} + service.lifecycle = lifecycle + root := t.TempDir() + if err := service.ConfigureArtifacts(root); err != nil { + t.Fatal(err) + } + content := []byte("test repository node rpm") + relativePath := "1.0.0/x86_64/ocmanager-repository-node-1.0.0-1.x86_64.rpm" + artifactPath := filepath.Join(root, filepath.FromSlash(relativePath)) + if err := os.MkdirAll(filepath.Dir(artifactPath), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(artifactPath, content, 0644); err != nil { + t.Fatal(err) + } + digest := sha256.Sum256(content) + spec := request.RepositoryNodeArtifactSpec{Name: "ocmanager-repository-node", Version: "1.0.0", + Release: "1", Architecture: "x86_64", FileName: filepath.Base(artifactPath), + SizeBytes: int64(len(content)), SHA256: hex.EncodeToString(digest[:]), + BuildCommit: "64afd0142a101d7e449424908dfa46d251f49f1e", RelativePath: relativePath} + artifact, err := service.RegisterArtifact(globalNodeContext(), spec) + if err != nil { + t.Fatal(err) + } + download, err := service.DownloadArtifact(globalNodeContext(), artifact.ID) + if err != nil { + t.Fatal(err) + } + defer download.Content.Close() + got, err := io.ReadAll(download.Content) + if err != nil { + t.Fatal(err) + } + if string(got) != string(content) || download.SHA256 != spec.SHA256 || download.SizeBytes != spec.SizeBytes { + t.Fatalf("artifact download = %#v, content = %q", download, got) + } + + spec.Version = "1.0.1" + spec.SHA256 = strings.Repeat("0", 64) + if _, err = service.RegisterArtifact(globalNodeContext(), spec); err == nil { + t.Fatal("RegisterArtifact() accepted metadata that does not match the stored RPM") + } +} + +func TestArtifactRegistrationRejectsSymlinkOutsideRoot(t *testing.T) { + service, _, sqlDB := newNodeTestService(t, &fakeNodeRepository{}) + defer sqlDB.Close() + service.lifecycle = &fakeLifecycleRepository{} + root, outside := t.TempDir(), filepath.Join(t.TempDir(), "outside.rpm") + if err := os.WriteFile(outside, []byte("outside"), 0644); err != nil { + t.Fatal(err) + } + if err := os.Symlink(outside, filepath.Join(root, "node.rpm")); err != nil { + t.Fatal(err) + } + if err := service.ConfigureArtifacts(root); err != nil { + t.Fatal(err) + } + digest := sha256.Sum256([]byte("outside")) + _, err := service.RegisterArtifact(globalNodeContext(), request.RepositoryNodeArtifactSpec{ + Name: "ocmanager-repository-node", Version: "1.0.0", Release: "1", Architecture: "x86_64", + FileName: "node.rpm", SizeBytes: 7, SHA256: hex.EncodeToString(digest[:]), + BuildCommit: "64afd01", RelativePath: "node.rpm", + }) + if err == nil { + t.Fatal("RegisterArtifact() followed a symlink outside the artifact root") + } +} + +type fakeCertificateIssuer struct{} + +func (fakeCertificateIssuer) Issue(_ []byte, nodeID, _ string) (request.RepositoryIssuedCertificate, error) { + now := time.Date(2026, 9, 20, 13, 0, 0, 0, time.UTC) + return request.RepositoryIssuedCertificate{PEM: []byte("certificate:" + nodeID), SerialNumber: "01", + Fingerprint: strings.Repeat("ab", 32), NotBefore: now.Add(-time.Minute), NotAfter: now.Add(90 * 24 * time.Hour)}, nil +} + +func (fakeCertificateIssuer) CACertificatePEM() []byte { return []byte("test-ca") } + +type fakeLifecycleRepository struct { + enrollment *po.RepositoryNodeEnrollment + token *po.RepositoryNodeBootstrapToken + certs []po.RepositoryNodeCertificate + artifact *po.RepositoryNodeArtifact + installSource *po.RepositoryNodeInstallSource + allowlist *po.RepositoryNodeRegistrationAllowlist + invalidatedEnrollmentID string +} + +func (f *fakeLifecycleRepository) SyncTables() error { return nil } +func (f *fakeLifecycleRepository) CreateEnrollment(value *po.RepositoryNodeEnrollment, _ entity.Session) error { + f.enrollment = value + return nil +} +func (f *fakeLifecycleRepository) UpdateEnrollment(value *po.RepositoryNodeEnrollment, _ entity.Session) error { + f.enrollment = value + return nil +} +func (f *fakeLifecycleRepository) GetEnrollmentByIDForUpdate(id string, _ entity.Session) (*po.RepositoryNodeEnrollment, error) { + if f.enrollment == nil || f.enrollment.ID != id { + return nil, repositorystoragenoderepo.ErrNodeEnrollmentNotFound + } + return f.enrollment, nil +} +func (f *fakeLifecycleRepository) FindLatestEnrollmentByNode(nodeID string, _ entity.Session) (*po.RepositoryNodeEnrollment, error) { + if f.enrollment == nil || f.enrollment.NodeID != nodeID { + return nil, repositorystoragenoderepo.ErrNodeEnrollmentNotFound + } + return f.enrollment, nil +} +func (f *fakeLifecycleRepository) FindEnrollmentByAgent(agentID uint64, _ entity.Session) (*po.RepositoryNodeEnrollment, error) { + if f.enrollment == nil || f.enrollment.TMSAgentID != agentID { + return nil, repositorystoragenoderepo.ErrNodeEnrollmentNotFound + } + return f.enrollment, nil +} +func (f *fakeLifecycleRepository) ListActiveInstallations(_ int, _ entity.Session) ([]po.RepositoryNodeEnrollment, error) { + if f.enrollment == nil || + (f.enrollment.Status != request.RepositoryNodeEnrollmentPending && + f.enrollment.Status != request.RepositoryNodeEnrollmentInstalling && + f.enrollment.Status != request.RepositoryNodeEnrollmentAuthorized && + f.enrollment.Status != request.RepositoryNodeEnrollmentRegistering && + f.enrollment.Status != request.RepositoryNodeEnrollmentRegistered && + f.enrollment.Status != request.RepositoryNodeEnrollmentBootstrapped) { + return nil, nil + } + return []po.RepositoryNodeEnrollment{*f.enrollment}, nil +} +func (f *fakeLifecycleRepository) CreateBootstrapToken(value *po.RepositoryNodeBootstrapToken, _ entity.Session) error { + f.token = value + return nil +} +func (f *fakeLifecycleRepository) GetBootstrapTokenByHashForUpdate(hash string, _ entity.Session) (*po.RepositoryNodeBootstrapToken, error) { + if f.token == nil || f.token.TokenHash != hash { + return nil, repositorystoragenoderepo.ErrNodeBootstrapNotFound + } + return f.token, nil +} +func (f *fakeLifecycleRepository) UpdateBootstrapToken(value *po.RepositoryNodeBootstrapToken, _ entity.Session) error { + f.token = value + return nil +} +func (f *fakeLifecycleRepository) InvalidateBootstrapTokens(enrollmentID string, at time.Time, _ entity.Session) error { + f.invalidatedEnrollmentID = enrollmentID + if f.token != nil { + f.token.InvalidatedAt = &at + } + return nil +} +func (f *fakeLifecycleRepository) CreateInstallSource(value *po.RepositoryNodeInstallSource, _ entity.Session) error { + f.installSource = value + return nil +} +func (f *fakeLifecycleRepository) UpdateInstallSource(value *po.RepositoryNodeInstallSource, _ entity.Session) error { + f.installSource = value + return nil +} +func (f *fakeLifecycleRepository) DeleteInstallSource(string, entity.Session) error { + f.installSource = nil + return nil +} +func (f *fakeLifecycleRepository) GetInstallSource(projectID int64, architecture string, _ bool, _ entity.Session) (*po.RepositoryNodeInstallSource, error) { + if f.installSource == nil || f.installSource.ProjectID != projectID || f.installSource.Architecture != architecture { + return nil, repositorystoragenoderepo.ErrNodeInstallSourceNotFound + } + return f.installSource, nil +} +func (f *fakeLifecycleRepository) ListInstallSources(projectID int64, _ entity.Session) ([]po.RepositoryNodeInstallSource, error) { + if f.installSource == nil || f.installSource.ProjectID != projectID { + return nil, nil + } + return []po.RepositoryNodeInstallSource{*f.installSource}, nil +} +func (f *fakeLifecycleRepository) CreateAllowlist(value *po.RepositoryNodeRegistrationAllowlist, _ entity.Session) error { + f.allowlist = value + return nil +} +func (f *fakeLifecycleRepository) UpdateAllowlist(value *po.RepositoryNodeRegistrationAllowlist, _ entity.Session) error { + f.allowlist = value + return nil +} +func (f *fakeLifecycleRepository) DeleteAllowlist(string, entity.Session) error { + f.allowlist = nil + return nil +} +func (f *fakeLifecycleRepository) GetAllowlist(projectID int64, agentID uint64, _ bool, _ entity.Session) (*po.RepositoryNodeRegistrationAllowlist, error) { + if f.allowlist == nil || f.allowlist.ProjectID != projectID || f.allowlist.TMSAgentID != agentID { + return nil, repositorystoragenoderepo.ErrNodeAllowlistNotFound + } + return f.allowlist, nil +} +func (f *fakeLifecycleRepository) ListAllowlists(projectID int64, _ entity.Session) ([]po.RepositoryNodeRegistrationAllowlist, error) { + if f.allowlist == nil || f.allowlist.ProjectID != projectID { + return nil, nil + } + return []po.RepositoryNodeRegistrationAllowlist{*f.allowlist}, nil +} +func (f *fakeLifecycleRepository) CreateCertificate(value *po.RepositoryNodeCertificate, _ entity.Session) error { + f.certs = append(f.certs, *value) + return nil +} +func (f *fakeLifecycleRepository) FindCertificateByAuthorizationID(authorizationID string, _ entity.Session) (*po.RepositoryNodeCertificate, error) { + for index := range f.certs { + if f.certs[index].AuthorizationID == authorizationID { + return &f.certs[index], nil + } + } + return nil, repositorystoragenoderepo.ErrNodeCertificateNotFound +} +func (f *fakeLifecycleRepository) SupersedeActiveCertificates(_ string, grace time.Time, _ entity.Session) error { + for index := range f.certs { + if f.certs[index].Status == request.RepositoryNodeCertificateActive { + f.certs[index].Status = request.RepositoryNodeCertificateSuperseded + f.certs[index].GraceUntil = &grace + } + } + return nil +} +func (f *fakeLifecycleRepository) FindUsableCertificate(nodeID, fingerprint string, now time.Time, _ entity.Session) (*po.RepositoryNodeCertificate, error) { + for index := range f.certs { + value := &f.certs[index] + if value.NodeID == nodeID && value.Fingerprint == fingerprint && now.Before(value.NotAfter) && + (value.Status == request.RepositoryNodeCertificateActive || + (value.Status == request.RepositoryNodeCertificateSuperseded && value.GraceUntil != nil && now.Before(*value.GraceUntil))) { + return value, nil + } + } + return nil, repositorystoragenoderepo.ErrNodeCertificateNotFound +} +func (f *fakeLifecycleRepository) CountCertificates(nodeID string, _ entity.Session) (int64, error) { + var count int64 + for index := range f.certs { + if f.certs[index].NodeID == nodeID { + count++ + } + } + return count, nil +} +func (f *fakeLifecycleRepository) RevokeCertificates(_ string, at time.Time, _ entity.Session) error { + for index := range f.certs { + f.certs[index].Status, f.certs[index].GraceUntil = request.RepositoryNodeCertificateRevoked, &at + } + return nil +} +func (f *fakeLifecycleRepository) CreateArtifact(value *po.RepositoryNodeArtifact, _ entity.Session) error { + f.artifact = value + return nil +} +func (f *fakeLifecycleRepository) UpdateArtifact(value *po.RepositoryNodeArtifact, _ entity.Session) error { + f.artifact = value + return nil +} +func (f *fakeLifecycleRepository) GetArtifactByID(id string, _ entity.Session) (*po.RepositoryNodeArtifact, error) { + if f.artifact == nil || f.artifact.ID != id { + return nil, repositorystoragenoderepo.ErrNodeArtifactNotFound + } + return f.artifact, nil +} +func (f *fakeLifecycleRepository) GetArtifact(version, release, architecture string, _ entity.Session) (*po.RepositoryNodeArtifact, error) { + if f.artifact == nil || f.artifact.Version != version || f.artifact.Release != release || f.artifact.Architecture != architecture { + return nil, repositorystoragenoderepo.ErrNodeArtifactNotFound + } + return f.artifact, nil +} +func (f *fakeLifecycleRepository) ListArtifacts(_ string, _ entity.Session) ([]po.RepositoryNodeArtifact, error) { + if f.artifact == nil { + return nil, nil + } + return []po.RepositoryNodeArtifact{*f.artifact}, nil +} +func (f *fakeLifecycleRepository) ListArtifactCandidates(projectID int64, architecture, sourceKind, status string, + _ entity.Session, +) ([]po.RepositoryNodeArtifact, error) { + if f.artifact == nil || f.artifact.ProjectID != projectID || f.artifact.Architecture != architecture || + f.artifact.SourceKind != sourceKind || f.artifact.Status != status { + return nil, nil + } + return []po.RepositoryNodeArtifact{*f.artifact}, nil +} + +type fakeLifecycleTaskRepository struct{ task *po.RepositoryTask } + +func (f *fakeLifecycleTaskRepository) SyncTables() error { return nil } +func (f *fakeLifecycleTaskRepository) Create(value *po.RepositoryTask, _ entity.Session) error { + f.task = value + return nil +} +func (f *fakeLifecycleTaskRepository) Update(value *po.RepositoryTask, _ entity.Session) error { + f.task = value + return nil +} +func (f *fakeLifecycleTaskRepository) GetByID(id string, _ entity.Session) (*po.RepositoryTask, error) { + if f.task == nil || f.task.ID != id { + return nil, repositorytaskrepo.ErrTaskNotFound + } + return f.task, nil +} +func (f *fakeLifecycleTaskRepository) GetByIDForUpdate(id string, session entity.Session) (*po.RepositoryTask, error) { + return f.GetByID(id, session) +} +func (f *fakeLifecycleTaskRepository) GetByIdempotencyKey(string, entity.Session) (*po.RepositoryTask, error) { + return nil, repositorytaskrepo.ErrTaskNotFound +} +func (f *fakeLifecycleTaskRepository) List(request.ListRepositoryTasksRequest, entity.Session) ([]po.RepositoryTask, request.PageResponse, error) { + return nil, request.PageResponse{}, nil +} +func (f *fakeLifecycleTaskRepository) HasUnfinishedMutation(string, entity.Session) (bool, error) { + return false, nil +} +func (f *fakeLifecycleTaskRepository) HasUnfinishedType(string, string, entity.Session) (bool, error) { + return false, nil +} +func (f *fakeLifecycleTaskRepository) FindClaimCandidate(time.Time, string, []string, entity.Session) (*po.RepositoryTask, error) { + return nil, repositorytaskrepo.ErrTaskNotFound +} +func (f *fakeLifecycleTaskRepository) FindExpired(time.Time, int, entity.Session) ([]po.RepositoryTask, error) { + return nil, nil +} + +type fakeLifecycleTargetRepository struct{ target *po.RepositoryTaskTarget } + +func (f *fakeLifecycleTargetRepository) SyncTables() error { return nil } +func (f *fakeLifecycleTargetRepository) CreateTarget(value *po.RepositoryTaskTarget, _ entity.Session) error { + f.target = value + return nil +} +func (f *fakeLifecycleTargetRepository) UpdateTarget(value *po.RepositoryTaskTarget, _ entity.Session) error { + f.target = value + return nil +} +func (f *fakeLifecycleTargetRepository) ListTargets(taskID string, _ entity.Session) ([]po.RepositoryTaskTarget, error) { + if f.target == nil || f.target.TaskID != taskID { + return nil, nil + } + return []po.RepositoryTaskTarget{*f.target}, nil +} diff --git a/manager/backend/services/footstone/domain/repositoryNodeService/liveness_integration_test.go b/manager/backend/services/footstone/domain/repositoryNodeService/liveness_integration_test.go new file mode 100644 index 0000000000000000000000000000000000000000..c8ba5c21318ffe6c2755a1d7d684dbee2ef4951e --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryNodeService/liveness_integration_test.go @@ -0,0 +1,83 @@ +//go:build integration + +package repositoryNodeService + +import ( + "fmt" + "os" + "testing" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + ftpb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/footstone" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +func TestListRefreshesOfflineSummaryAndHeartbeatWithMySQL(t *testing.T) { + dsn := os.Getenv("OCM_REPOSITORY_TEST_DSN") + if dsn == "" { + t.Skip("OCM_REPOSITORY_TEST_DSN is not set") + } + db, err := database.NewDBClient(config.AccessPoint{Source: dsn}) + if err != nil { + t.Fatal(err) + } + defer db.Close() + const projectID int64 = 920087 + s := New(db) + if err = s.SyncTables(); err != nil { + t.Fatal(err) + } + if err = db.DB().AutoMigrate(&po.Project{}, &po.SoftwareRepository{}); err != nil { + t.Fatal(err) + } + defer db.DB().Where("project_id = ?", projectID).Delete(&po.RepositoryStorageNode{}) + defer db.DB().Where("id = ?", projectID).Delete(&po.Project{}) + if err = db.DB().Create(&po.Project{Base: po.Base{ID: projectID}, Name: "liveness-mysql", EnName: "liveness-mysql", Creator: "liveness-admin"}).Error; err != nil { + t.Fatal(err) + } + now := time.Now().UTC().Truncate(time.Second) + s.now = func() time.Time { return now } + stale := now.Add(-31 * time.Second) + for index := 1; index <= 3; index++ { + heartbeat, status, enabled := now, "ONLINE", true + if index == 1 { + heartbeat = stale + } + if index == 3 { + heartbeat, status, enabled = stale, "DISABLED", false + } + node := &po.RepositoryStorageNode{ID: fmt.Sprintf("01234567-89ab-4cde-8123-%012d", index), ProjectID: projectID, + Name: fmt.Sprintf("liveness-%d", index), ControlBaseURL: fmt.Sprintf("https://liveness-%d.invalid", index), + PublishBaseURL: fmt.Sprintf("http://liveness-%d.invalid", index), Status: status, Enabled: enabled, + CapacityBytes: 1000, FreeBytes: 400, FreeInodes: 100, LastHeartbeatAt: &heartbeat, + ToolVersionsJSON: []byte(`{}`), LabelsJSON: []byte(`{}`), CreatedBy: "liveness-admin"} + if err = db.DB().Create(node).Error; err != nil { + t.Fatal(err) + } + } + ctx := entity.NewDefaultContext() + ctx.User = &ftpb.UserInfo{LoginName: "liveness-admin"} + filter := request.ListRepositoryStorageNodesRequest{ProjectID: projectID, PageRequest: request.PageRequest{PageNumber: 1, PageSize: 1}} + rows, _, stats, err := s.ListWithStatistics(ctx, filter) + if err != nil || len(rows) != 1 || stats.TotalNodes != 3 || stats.OnlineNodes != 1 || stats.TotalCapacityBytes != 3000 || stats.UsedCapacityBytes != 1800 { + t.Fatalf("full summary with page size one: rows=%d stats=%+v error=%v", len(rows), stats, err) + } + filter.Status = "OFFLINE" + rows, _, stats, err = s.ListWithStatistics(ctx, filter) + if err != nil || len(rows) != 1 || stats.TotalNodes != 1 || stats.OnlineNodes != 0 || rows[0].CapacityBytes != 1000 { + t.Fatalf("offline filter: rows=%+v stats=%+v error=%v", rows, stats, err) + } + if _, err = s.RecordHeartbeat(request.RepositoryStorageNodeHeartbeat{NodeID: rows[0].ID, FilesystemType: "xfs", CapacityBytes: 1000, FreeBytes: 400, FreeInodes: 100}); err != nil { + t.Fatal(err) + } + filter.Status = "ONLINE" + _, _, stats, err = s.ListWithStatistics(ctx, filter) + if err != nil || stats.TotalNodes != 2 || stats.OnlineNodes != 2 { + t.Fatalf("heartbeat recovery: %+v/%v", stats, err) + } + t.Log("full-set summary, stale OFFLINE filter, preserved capacity, disabled node and heartbeat recovery PASS") +} diff --git a/manager/backend/services/footstone/domain/repositoryNodeService/liveness_test.go b/manager/backend/services/footstone/domain/repositoryNodeService/liveness_test.go new file mode 100644 index 0000000000000000000000000000000000000000..532f351512eafbb5b3b82e029172a3bc0a21ea97 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryNodeService/liveness_test.go @@ -0,0 +1,54 @@ +package repositoryNodeService + +import ( + "testing" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +type livenessNodeRepository struct{ fakeNodeRepository } + +func (r *livenessNodeRepository) MarkOfflineBefore(cutoff time.Time, session entity.Session) (int64, error) { + r.offlineCutoff = cutoff + if r.node.Enabled && r.node.Status == request.RepositoryStorageNodeStatusOnline && + (r.node.LastHeartbeatAt == nil || r.node.LastHeartbeatAt.Before(cutoff)) { + r.node.Status = request.RepositoryStorageNodeStatusOffline + return 1, nil + } + return 0, nil +} + +func TestDescribeRefreshesExpiredLivenessWithoutProbe(t *testing.T) { + now := time.Now().UTC() + for _, test := range []struct { + name string + age time.Duration + status string + enabled bool + want string + }{ + {"fresh", 29 * time.Second, "ONLINE", true, "ONLINE"}, + {"boundary", 30 * time.Second, "ONLINE", true, "ONLINE"}, + {"expired", 31 * time.Second, "ONLINE", true, "OFFLINE"}, + {"disabled", time.Hour, "DISABLED", false, "DISABLED"}, + {"unknown", time.Hour, "UNKNOWN", true, "UNKNOWN"}, + } { + t.Run(test.name, func(t *testing.T) { + heartbeat := now.Add(-test.age) + records := &livenessNodeRepository{fakeNodeRepository{node: &po.RepositoryStorageNode{ + ID: testNodeID, ProjectID: 42, Status: test.status, Enabled: test.enabled, + LastHeartbeatAt: &heartbeat, CapacityBytes: 1000, FreeBytes: 500, + }}} + s, _, db := newNodeTestService(t, records) + defer db.Close() + s.now = func() time.Time { return now } + value, err := s.Describe(globalNodeContext(), testNodeID) + if err != nil || value.Status != test.want || value.CapacityBytes != 1000 { + t.Fatalf("Describe() = %#v, %v; want status %s with preserved capacity", value, err, test.want) + } + }) + } +} diff --git a/manager/backend/services/footstone/domain/repositoryNodeService/node_rpm.go b/manager/backend/services/footstone/domain/repositoryNodeService/node_rpm.go new file mode 100644 index 0000000000000000000000000000000000000000..14e939bccc1a3ce1a3346ec1a03150fdf46dafef --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryNodeService/node_rpm.go @@ -0,0 +1,417 @@ +package repositoryNodeService + +import ( + "errors" + "fmt" + "net/url" + "path" + "strconv" + "strings" + "time" + + "github.com/google/uuid" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorystoragenoderepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryNodePlatform" +) + +const repositoryNodePackageName = "ocmanager-repository-node" + +var ( + ErrNodeInstallSourceInvalid = errors.New("repository node install source is invalid") + ErrNodeArtifactInvalid = errors.New("repository-backed node artifact is invalid") + ErrNodeRegistrationDenied = errors.New("repository node registration is not allowed") +) + +type NodeRPMSelection struct { + Artifact *po.RepositoryNodeArtifact + Repository *po.SoftwareRepository + Revision *po.RepositoryRevision + RevisionURL string +} + +// ConfigureInstallSource binds one canonical architecture to a protected, +// ordinary private Repository. The Repository continues to use the existing +// upload, verification, revision and publication pipeline. +func (s *service) ConfigureInstallSource(ctx entity.Context, input request.RepositoryNodeInstallSourceSpec) ( + value *po.RepositoryNodeInstallSource, err error, +) { + if s.lifecycle == nil || s.repositories == nil { + return nil, ErrNodeLifecycleDisabled + } + architecture, err := repositoryNodePlatform.NormalizeArchitecture(input.Architecture) + if err != nil { + return nil, err + } + status := strings.ToUpper(strings.TrimSpace(input.Status)) + if status == "" { + status = request.RepositoryNodeInstallSourceEnabled + } + if status != request.RepositoryNodeInstallSourceEnabled && status != request.RepositoryNodeInstallSourceDisabled { + return nil, fmt.Errorf("%w: unsupported status", ErrNodeInstallSourceInvalid) + } + if uuid.Validate(strings.TrimSpace(input.RepositoryID)) != nil { + return nil, fmt.Errorf("%w: repository_id must be a UUID", ErrNodeInstallSourceInvalid) + } + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + if err = s.authorizeProject(ctx, input.ProjectID, tx); err != nil { + return nil, err + } + repository, err := s.repositories.GetByID(strings.TrimSpace(input.RepositoryID), tx) + if err != nil { + return nil, err + } + if err = validateNodeRPMRepository(repository, input.ProjectID, architecture, false); err != nil { + return nil, err + } + if !repository.SystemManaged || repository.Purpose != request.RepositoryPurposeNodeRPM { + if err = s.repositories.UpdateSystemPurpose(repository.ID, true, request.RepositoryPurposeNodeRPM, tx); err != nil { + return nil, err + } + } + value, err = s.lifecycle.GetInstallSource(input.ProjectID, architecture, true, tx) + if err != nil && !errors.Is(err, repositorystoragenoderepo.ErrNodeInstallSourceNotFound) { + return nil, err + } + if value == nil { + value = &po.RepositoryNodeInstallSource{ID: uuid.NewString(), ProjectID: input.ProjectID, + RepositoryID: repository.ID, Architecture: architecture, Status: status, + CreatedBy: ctx.User.LoginName, UpdatedBy: ctx.User.LoginName} + if err = s.lifecycle.CreateInstallSource(value, tx); err != nil { + return nil, err + } + return value, nil + } + value.RepositoryID, value.Status, value.UpdatedBy = repository.ID, status, ctx.User.LoginName + if err = s.lifecycle.UpdateInstallSource(value, tx); err != nil { + return nil, err + } + return value, nil +} + +func (s *service) ListInstallSources(ctx entity.Context, projectID int64) ( + []po.RepositoryNodeInstallSource, error, +) { + if s.lifecycle == nil { + return nil, ErrNodeLifecycleDisabled + } + if err := s.authorizeProject(ctx, projectID, nil); err != nil { + return nil, err + } + return s.lifecycle.ListInstallSources(projectID, nil) +} + +func (s *service) UpsertRegistrationAllowlist(ctx entity.Context, + input request.RepositoryNodeRegistrationAllowlistSpec, +) (value *po.RepositoryNodeRegistrationAllowlist, err error) { + if s.lifecycle == nil { + return nil, ErrNodeLifecycleDisabled + } + if input.ProjectID <= 0 || input.TMSAgentID == 0 { + return nil, fmt.Errorf("%w: project_id and tms_agent_id are required", ErrNodeRegistrationDenied) + } + architecture := "" + if strings.TrimSpace(input.ExpectedArchitecture) != "" { + architecture, err = repositoryNodePlatform.NormalizeArchitecture(input.ExpectedArchitecture) + if err != nil { + return nil, err + } + } + status := strings.ToUpper(strings.TrimSpace(input.Status)) + if status == "" { + status = request.RepositoryNodeAllowlistEnabled + } + if status != request.RepositoryNodeAllowlistEnabled && status != request.RepositoryNodeAllowlistDisabled { + return nil, fmt.Errorf("%w: unsupported allowlist status", ErrNodeRegistrationDenied) + } + now := s.now() + if input.ExpiresAt != nil && !input.ExpiresAt.After(now) && status == request.RepositoryNodeAllowlistEnabled { + return nil, fmt.Errorf("%w: enabled allowlist entry is already expired", ErrNodeRegistrationDenied) + } + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + if err = s.authorizeProject(ctx, input.ProjectID, tx); err != nil { + return nil, err + } + value, err = s.lifecycle.GetAllowlist(input.ProjectID, input.TMSAgentID, true, tx) + if err != nil && !errors.Is(err, repositorystoragenoderepo.ErrNodeAllowlistNotFound) { + return nil, err + } + creating := value == nil + if value == nil { + value = &po.RepositoryNodeRegistrationAllowlist{ID: uuid.NewString(), ProjectID: input.ProjectID, + TMSAgentID: input.TMSAgentID, CreatedBy: ctx.User.LoginName} + } + value.ExpectedHostname = strings.TrimSpace(input.ExpectedHostname) + value.ExpectedIP = strings.TrimSpace(input.ExpectedIP) + value.ExpectedArchitecture = architecture + value.Status, value.ExpiresAt, value.UpdatedBy = status, input.ExpiresAt, ctx.User.LoginName + if creating { + if err = s.lifecycle.CreateAllowlist(value, tx); err != nil { + return nil, err + } + } else if err = s.lifecycle.UpdateAllowlist(value, tx); err != nil { + return nil, err + } + return value, nil +} + +func (s *service) ListRegistrationAllowlists(ctx entity.Context, projectID int64) ( + []po.RepositoryNodeRegistrationAllowlist, error, +) { + if s.lifecycle == nil { + return nil, ErrNodeLifecycleDisabled + } + if err := s.authorizeProject(ctx, projectID, nil); err != nil { + return nil, err + } + return s.lifecycle.ListAllowlists(projectID, nil) +} + +func validateRegistrationAllowlist(value *po.RepositoryNodeRegistrationAllowlist, projectID int64, + agentID uint64, architecture string, now time.Time, +) error { + if value == nil || value.ProjectID != projectID || value.TMSAgentID != agentID || + value.Status != request.RepositoryNodeAllowlistEnabled || + (value.ExpiresAt != nil && !now.Before(*value.ExpiresAt)) { + return ErrNodeRegistrationDenied + } + if value.ExpectedArchitecture != "" && value.ExpectedArchitecture != architecture { + return fmt.Errorf("%w: architecture does not match allowlist", ErrNodeRegistrationDenied) + } + return nil +} + +func validateAllowlistObservation(value *po.RepositoryNodeRegistrationAllowlist, observedHostname, observedIP string) error { + if value.ExpectedHostname != "" && !strings.EqualFold(value.ExpectedHostname, strings.TrimSpace(observedHostname)) { + return fmt.Errorf("%w: hostname does not match allowlist", ErrNodeRegistrationDenied) + } + if value.ExpectedIP != "" && value.ExpectedIP != strings.TrimSpace(observedIP) { + return fmt.Errorf("%w: IP address does not match allowlist", ErrNodeRegistrationDenied) + } + return nil +} + +// RegisterRepositoryArtifact creates the management identity for a Node RPM +// that has already passed the normal Repository upload and publish pipeline. +func (s *service) RegisterRepositoryArtifact(ctx entity.Context, input request.RepositoryNodeArtifactSpec) ( + value *po.RepositoryNodeArtifact, err error, +) { + if s.lifecycle == nil || s.repositories == nil || s.content == nil { + return nil, ErrNodeLifecycleDisabled + } + architecture, err := repositoryNodePlatform.NormalizeArchitecture(input.Architecture) + if err != nil { + return nil, err + } + if input.ProjectID <= 0 || uuid.Validate(input.SourceRepositoryID) != nil || + uuid.Validate(input.RevisionID) != nil || uuid.Validate(input.PackageID) != nil { + return nil, fmt.Errorf("%w: project and immutable source identifiers are required", ErrNodeArtifactInvalid) + } + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + if err = s.authorizeProject(ctx, input.ProjectID, tx); err != nil { + return nil, err + } + source, err := s.lifecycle.GetInstallSource(input.ProjectID, architecture, false, tx) + if err != nil || source.Status != request.RepositoryNodeInstallSourceEnabled || + source.RepositoryID != input.SourceRepositoryID { + return nil, fmt.Errorf("%w: repository is not the enabled install source", ErrNodeArtifactInvalid) + } + repository, err := s.repositories.GetByID(input.SourceRepositoryID, tx) + if err != nil { + return nil, err + } + if err = validateNodeRPMRepository(repository, input.ProjectID, architecture, true); err != nil { + return nil, err + } + revision, err := s.content.GetRevision(repository.ID, input.RevisionID, tx) + if err != nil || revision.Status != request.RepositoryRevisionStatusPublished || revision.PublishedAt == nil { + return nil, fmt.Errorf("%w: revision is not published", ErrNodeArtifactInvalid) + } + packages, err := s.content.PackagesByRevision(revision.ID, tx) + if err != nil { + return nil, err + } + var packageValue *po.RepositoryPackage + for index := range packages { + if packages[index].ID == input.PackageID { + packageValue = &packages[index] + break + } + } + if packageValue == nil || packageValue.Name != repositoryNodePackageName { + return nil, fmt.Errorf("%w: package is not %s", ErrNodeArtifactInvalid, repositoryNodePackageName) + } + packageArchitecture, err := repositoryNodePlatform.NormalizeArchitecture(packageValue.Architecture) + if err != nil || packageArchitecture != architecture { + return nil, fmt.Errorf("%w: package architecture does not match install source", ErrNodeArtifactInvalid) + } + epoch, err := strconv.ParseUint(strings.TrimSpace(packageValue.Epoch), 10, 64) + if err != nil { + return nil, fmt.Errorf("%w: package epoch is invalid", ErrNodeArtifactInvalid) + } + immutableURL, err := nodeRPMImmutablePackageURL(repository, revision.ID, packageValue.RelativePath) + if err != nil { + return nil, err + } + value = &po.RepositoryNodeArtifact{ + ID: uuid.NewString(), ProjectID: input.ProjectID, SourceKind: request.RepositoryNodeArtifactRepository, + SourceRepositoryID: repository.ID, RevisionID: revision.ID, PackageID: packageValue.ID, + Name: packageValue.Name, Epoch: epoch, Version: packageValue.Version, Release: packageValue.Release, + Architecture: architecture, NEVRA: packageValue.NEVRA, FileName: packageValue.FileName, + SizeBytes: packageValue.SizeBytes, SHA256: strings.ToLower(packageValue.SHA256), + BuildCommit: strings.TrimSpace(input.BuildCommit), RelativePath: packageValue.RelativePath, + ImmutableURL: immutableURL, Status: request.RepositoryNodeArtifactPublished, + } + if value.BuildCommit == "" { + value.BuildCommit = "repository-revision:" + revision.ID + } + if err = s.lifecycle.CreateArtifact(value, tx); err != nil { + return nil, err + } + return value, nil +} + +// SelectInstallArtifact applies all server-side selection constraints. A +// requested historical Artifact is accepted only if it is still backed by the +// currently enabled project/architecture install source. +func (s *service) SelectInstallArtifact(ctx entity.Context, projectID int64, architecture, artifactID string) ( + *NodeRPMSelection, error, +) { + if s.lifecycle == nil || s.repositories == nil || s.content == nil { + return nil, ErrNodeLifecycleDisabled + } + canonical, err := repositoryNodePlatform.NormalizeArchitecture(architecture) + if err != nil { + return nil, err + } + if err = s.authorizeProject(ctx, projectID, nil); err != nil { + return nil, err + } + source, err := s.lifecycle.GetInstallSource(projectID, canonical, false, nil) + if err != nil || source.Status != request.RepositoryNodeInstallSourceEnabled { + return nil, repositorystoragenoderepo.ErrNodeInstallSourceNotFound + } + repository, err := s.repositories.GetByID(source.RepositoryID, nil) + if err != nil { + return nil, err + } + if err = validateNodeRPMRepository(repository, projectID, canonical, true); err != nil { + return nil, err + } + values, err := s.lifecycle.ListArtifactCandidates(projectID, canonical, + request.RepositoryNodeArtifactRepository, request.RepositoryNodeArtifactPublished, nil) + if err != nil { + return nil, err + } + var selected *po.RepositoryNodeArtifact + for index := range values { + candidate := &values[index] + if candidate.SourceRepositoryID != source.RepositoryID || candidate.Name != repositoryNodePackageName { + continue + } + if artifactID != "" && candidate.ID != artifactID { + continue + } + comparison := 1 + if selected != nil { + comparison, err = repositoryNodePlatform.CompareEVR( + repositoryNodePlatform.EVR{Epoch: strconv.FormatUint(candidate.Epoch, 10), Version: candidate.Version, Release: candidate.Release}, + repositoryNodePlatform.EVR{Epoch: strconv.FormatUint(selected.Epoch, 10), Version: selected.Version, Release: selected.Release}, + ) + if err != nil { + return nil, fmt.Errorf("%w: compare RPM EVR: %v", ErrNodeArtifactInvalid, err) + } + } + if selected == nil || comparison > 0 { + selected = candidate + } + } + if selected == nil { + return nil, repositorystoragenoderepo.ErrNodeArtifactNotFound + } + revision, err := s.content.GetRevision(repository.ID, selected.RevisionID, nil) + if err != nil || revision.Status != request.RepositoryRevisionStatusPublished || revision.PublishedAt == nil { + return nil, fmt.Errorf("%w: immutable revision is unavailable", ErrNodeArtifactInvalid) + } + expectedURL, err := nodeRPMImmutablePackageURL(repository, revision.ID, selected.RelativePath) + if err != nil { + return nil, fmt.Errorf("%w: immutable package URL no longer matches its revision", ErrNodeArtifactInvalid) + } + // immutable_url is derived from the published Revision and package path. Repair + // records created by older builds that joined the storage-root relative path + // onto the Revision URL twice; the immutable Repository remains authoritative. + if expectedURL != selected.ImmutableURL { + selected.ImmutableURL = expectedURL + if err = s.lifecycle.UpdateArtifact(selected, nil); err != nil { + return nil, fmt.Errorf("%w: repair immutable package URL: %v", ErrNodeArtifactInvalid, err) + } + } + revisionURL, err := nodeRPMImmutableRevisionURL(repository, revision.ID) + if err != nil { + return nil, err + } + return &NodeRPMSelection{Artifact: selected, Repository: repository, Revision: revision, + RevisionURL: revisionURL}, nil +} + +func validateNodeRPMRepository(repository *po.SoftwareRepository, projectID int64, architecture string, + requireSystemPurpose bool, +) error { + if repository == nil || repository.ProjectID != projectID || repository.Architecture != architecture || + repository.Type != request.RepositoryTypePrivate || !repository.AllowUpload || + strings.TrimSpace(repository.UpstreamURL) != "" || !repository.Enabled { + return fmt.Errorf("%w: a private upload-only repository with matching architecture is required", + ErrNodeInstallSourceInvalid) + } + if requireSystemPurpose && (!repository.SystemManaged || repository.Purpose != request.RepositoryPurposeNodeRPM) { + return fmt.Errorf("%w: repository is not protected for NODE_RPM", ErrNodeInstallSourceInvalid) + } + return nil +} + +func nodeRPMImmutablePackageURL(repository *po.SoftwareRepository, revisionID, relativePath string) (string, error) { + revisionURL, err := nodeRPMImmutableRevisionURL(repository, revisionID) + if err != nil { + return "", err + } + base, err := url.Parse(revisionURL) + if err != nil { + return "", fmt.Errorf("%w: immutable revision URL is invalid", ErrNodeArtifactInvalid) + } + cleanPath := path.Clean(strings.TrimSpace(relativePath)) + if cleanPath == "." || cleanPath == ".." || strings.HasPrefix(cleanPath, "../") || + strings.HasPrefix(cleanPath, "/") { + return "", fmt.Errorf("%w: package relative path is invalid", ErrNodeArtifactInvalid) + } + storagePrefix := path.Join("public", repository.PublishKey, "revisions", revisionID) + "/" + if strings.HasPrefix(cleanPath, storagePrefix) { + cleanPath = strings.TrimPrefix(cleanPath, storagePrefix) + } + if cleanPath == "" || cleanPath == "." || cleanPath == ".." || strings.HasPrefix(cleanPath, "../") { + return "", fmt.Errorf("%w: package path inside revision is invalid", ErrNodeArtifactInvalid) + } + base.Path = strings.TrimRight(base.Path, "/") + "/" + strings.TrimLeft(cleanPath, "/") + base.RawPath = "" + return base.String(), nil +} + +func nodeRPMImmutableRevisionURL(repository *po.SoftwareRepository, revisionID string) (string, error) { + base, err := url.Parse(strings.TrimSpace(repository.PublishBaseURLSnapshot)) + if err != nil || (base.Scheme != "http" && base.Scheme != "https") || base.Host == "" || + base.User != nil || base.RawQuery != "" || base.Fragment != "" { + return "", fmt.Errorf("%w: publish base URL is invalid", ErrNodeArtifactInvalid) + } + if strings.TrimSpace(repository.PublishKey) == "" || uuid.Validate(revisionID) != nil { + return "", fmt.Errorf("%w: publish key or revision identity is invalid", ErrNodeArtifactInvalid) + } + base.Path = strings.TrimRight(base.Path, "/") + "/" + url.PathEscape(repository.PublishKey) + + "/revisions/" + url.PathEscape(revisionID) + base.RawPath = "" + return base.String(), nil +} diff --git a/manager/backend/services/footstone/domain/repositoryNodeService/node_rpm_test.go b/manager/backend/services/footstone/domain/repositoryNodeService/node_rpm_test.go new file mode 100644 index 0000000000000000000000000000000000000000..8f1d3a06064cd4f153573b30cd7de4fb2891b191 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryNodeService/node_rpm_test.go @@ -0,0 +1,186 @@ +package repositoryNodeService + +import ( + "errors" + "testing" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorycontentrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositoryrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorystoragenoderepo" +) + +const ( + nodeRPMRepositoryID = "11111111-1111-4111-8111-111111111111" + nodeRPMRevisionID = "22222222-2222-4222-8222-222222222222" +) + +func TestSelectInstallArtifactUsesRPMEVRAndSupportsPinnedHistory(t *testing.T) { + publishedAt := time.Now().UTC() + repository := validNodeRPMRepository() + revision := &po.RepositoryRevision{ID: nodeRPMRevisionID, RepositoryID: repository.ID, + Status: request.RepositoryRevisionStatusPublished, PublishedAt: &publishedAt} + old := nodeRPMArtifact("33333333-3333-4333-8333-333333333333", "1.9.0", "1") + latest := nodeRPMArtifact("44444444-4444-4444-8444-444444444444", "1.10.0", "1") + service := &service{projects: fakeNodeProjectReader{}, now: time.Now, + lifecycle: &nodeRPMLifecycle{source: &po.RepositoryNodeInstallSource{ProjectID: 42, + RepositoryID: repository.ID, Architecture: "x86_64", Status: request.RepositoryNodeInstallSourceEnabled}, + artifacts: []po.RepositoryNodeArtifact{old, latest}}, + repositories: &nodeRPMRepositories{value: repository}, + content: &nodeRPMContent{revision: revision}} + + selected, err := service.SelectInstallArtifact(globalNodeContext(), 42, "amd64", "") + if err != nil { + t.Fatal(err) + } + if selected.Artifact.ID != latest.ID || selected.RevisionURL == "" { + t.Fatalf("latest selection = %#v", selected) + } + pinned, err := service.SelectInstallArtifact(globalNodeContext(), 42, "x64", old.ID) + if err != nil { + t.Fatal(err) + } + if pinned.Artifact.ID != old.ID { + t.Fatalf("historical selection = %#v", pinned.Artifact) + } +} + +func TestSelectInstallArtifactRejectsUnboundAndCrossProjectArtifact(t *testing.T) { + repository := validNodeRPMRepository() + service := &service{projects: fakeNodeProjectReader{}, now: time.Now, + lifecycle: &nodeRPMLifecycle{source: &po.RepositoryNodeInstallSource{ProjectID: 42, + RepositoryID: repository.ID, Architecture: "x86_64", Status: request.RepositoryNodeInstallSourceEnabled}, + artifacts: []po.RepositoryNodeArtifact{nodeRPMArtifact( + "33333333-3333-4333-8333-333333333333", "2.0", "1")}}, + repositories: &nodeRPMRepositories{value: repository}, + content: &nodeRPMContent{revision: &po.RepositoryRevision{}}} + service.lifecycle.(*nodeRPMLifecycle).artifacts[0].ProjectID = 99 + if _, err := service.SelectInstallArtifact(globalNodeContext(), 42, "x86_64", ""); !errors.Is(err, repositorystoragenoderepo.ErrNodeArtifactNotFound) { + t.Fatalf("cross-project artifact error = %v", err) + } +} + +func TestNodeRPMRepositoryRequiresProtectedUploadOnlyPrivateRepository(t *testing.T) { + value := validNodeRPMRepository() + if err := validateNodeRPMRepository(value, 42, "x86_64", true); err != nil { + t.Fatal(err) + } + value.UpstreamURL = "https://mirror.example/repo" + if err := validateNodeRPMRepository(value, 42, "x86_64", true); !errors.Is(err, ErrNodeInstallSourceInvalid) { + t.Fatalf("upstream-backed system repository error = %v", err) + } +} + +func TestNodeRPMImmutablePackageURLStripsStorageRootPrefix(t *testing.T) { + repository := validNodeRPMRepository() + storagePath := "public/" + repository.PublishKey + "/revisions/" + nodeRPMRevisionID + + "/digest-ocmanager-repository-node.rpm" + value, err := nodeRPMImmutablePackageURL(repository, nodeRPMRevisionID, storagePath) + if err != nil { + t.Fatal(err) + } + expected := "https://repo.example/repositories/node-rpm-x86-64/revisions/" + nodeRPMRevisionID + + "/digest-ocmanager-repository-node.rpm" + if value != expected { + t.Fatalf("immutable package URL = %q, want %q", value, expected) + } +} + +func TestRegistrationAllowlistObservationChecksOptionalIdentityConstraints(t *testing.T) { + value := &po.RepositoryNodeRegistrationAllowlist{ + ExpectedHostname: "repo-node.example.internal", + ExpectedIP: "192.0.2.25", + } + if err := validateAllowlistObservation(value, "REPO-NODE.EXAMPLE.INTERNAL", "192.0.2.25"); err != nil { + t.Fatal(err) + } + if err := validateAllowlistObservation(value, "other.example.internal", "192.0.2.25"); !errors.Is(err, ErrNodeRegistrationDenied) { + t.Fatalf("hostname mismatch error = %v", err) + } + if err := validateAllowlistObservation(value, "repo-node.example.internal", "192.0.2.26"); !errors.Is(err, ErrNodeRegistrationDenied) { + t.Fatalf("IP mismatch error = %v", err) + } +} + +func validNodeRPMRepository() *po.SoftwareRepository { + return &po.SoftwareRepository{ID: nodeRPMRepositoryID, ProjectID: 42, Type: request.RepositoryTypePrivate, + Architecture: "x86_64", AllowUpload: true, Enabled: true, SystemManaged: true, + Purpose: request.RepositoryPurposeNodeRPM, PublishKey: "node-rpm-x86-64", + PublishBaseURLSnapshot: "https://repo.example/repositories"} +} + +func nodeRPMArtifact(id, version, release string) po.RepositoryNodeArtifact { + value := po.RepositoryNodeArtifact{ID: id, ProjectID: 42, SourceKind: request.RepositoryNodeArtifactRepository, + SourceRepositoryID: nodeRPMRepositoryID, RevisionID: nodeRPMRevisionID, + Name: repositoryNodePackageName, Version: version, Release: release, Architecture: "x86_64", + RelativePath: "Packages/ocmanager-repository-node.rpm", Status: request.RepositoryNodeArtifactPublished} + value.ImmutableURL, _ = nodeRPMImmutablePackageURL(validNodeRPMRepository(), value.RevisionID, value.RelativePath) + return value +} + +type nodeRPMLifecycle struct { + repositorystoragenoderepo.LifecycleRepository + source *po.RepositoryNodeInstallSource + artifacts []po.RepositoryNodeArtifact +} + +func (f *nodeRPMLifecycle) UpdateArtifact(value *po.RepositoryNodeArtifact, _ entity.Session) error { + for index := range f.artifacts { + if f.artifacts[index].ID == value.ID { + f.artifacts[index] = *value + return nil + } + } + return repositorystoragenoderepo.ErrNodeArtifactNotFound +} + +func (f *nodeRPMLifecycle) GetInstallSource(projectID int64, architecture string, _ bool, + _ entity.Session, +) (*po.RepositoryNodeInstallSource, error) { + if f.source == nil || f.source.ProjectID != projectID || f.source.Architecture != architecture { + return nil, repositorystoragenoderepo.ErrNodeInstallSourceNotFound + } + return f.source, nil +} + +func (f *nodeRPMLifecycle) ListArtifactCandidates(projectID int64, architecture, sourceKind, status string, + _ entity.Session, +) ([]po.RepositoryNodeArtifact, error) { + result := make([]po.RepositoryNodeArtifact, 0, len(f.artifacts)) + for _, value := range f.artifacts { + if value.ProjectID == projectID && value.Architecture == architecture && + value.SourceKind == sourceKind && value.Status == status { + result = append(result, value) + } + } + return result, nil +} + +type nodeRPMRepositories struct { + repositoryrepo.Repository + value *po.SoftwareRepository +} + +func (f *nodeRPMRepositories) GetByID(id string, _ entity.Session) (*po.SoftwareRepository, error) { + if f.value == nil || f.value.ID != id { + return nil, repositoryrepo.ErrRepositoryNotFound + } + return f.value, nil +} + +type nodeRPMContent struct { + repositorycontentrepo.Repository + revision *po.RepositoryRevision +} + +func (f *nodeRPMContent) GetRevision(repositoryID, revisionID string, + _ entity.Session, +) (*po.RepositoryRevision, error) { + if f.revision == nil || f.revision.RepositoryID != repositoryID || f.revision.ID != revisionID { + return nil, repositorycontentrepo.ErrRevisionNotFound + } + return f.revision, nil +} diff --git a/manager/backend/services/footstone/domain/repositoryNodeService/offline_context_test.go b/manager/backend/services/footstone/domain/repositoryNodeService/offline_context_test.go new file mode 100644 index 0000000000000000000000000000000000000000..ca04dab614c3beacdbb9a12057885c32dd415783 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryNodeService/offline_context_test.go @@ -0,0 +1,50 @@ +package repositoryNodeService + +import ( + "context" + "errors" + "testing" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" +) + +type offlineContextRepository struct { + fakeNodeRepository + ctx context.Context +} + +func (r *offlineContextRepository) MarkOfflineBefore(cutoff time.Time, session entity.Session) (int64, error) { + r.offlineCutoff = cutoff + r.ctx = session.Statement.Context + return 2, nil +} + +func TestMarkOfflineContextPassesLifecycleContextToDatabase(t *testing.T) { + repository := &offlineContextRepository{} + service, mock, sqlDB := newNodeTestService(t, &repository.fakeNodeRepository) + defer sqlDB.Close() + service.nodes = repository + now := time.Now().UTC() + service.now = func() time.Time { return now } + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + if count, err := service.MarkOfflineContext(ctx); err != nil || count != 2 { + t.Fatalf("offline reconciliation count/error = %d/%v", count, err) + } + if repository.ctx != ctx || !repository.offlineCutoff.Equal(now.Add(-offlineAfter)) { + t.Fatal("offline reconciliation lost context or heartbeat cutoff") + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } +} + +func TestMarkOfflineContextDoesNotQueryAfterShutdown(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + service := &service{} + if count, err := service.MarkOfflineContext(ctx); count != 0 || !errors.Is(err, context.Canceled) { + t.Fatalf("shutdown offline reconciliation count/error = %d/%v", count, err) + } +} diff --git a/manager/backend/services/footstone/domain/repositoryNodeService/service.go b/manager/backend/services/footstone/domain/repositoryNodeService/service.go new file mode 100644 index 0000000000000000000000000000000000000000..30540b16887e561a096792d3a76ac1371fc729fb --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryNodeService/service.go @@ -0,0 +1,1108 @@ +package repositoryNodeService + +import ( + "context" + "crypto/sha256" + "crypto/subtle" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "math" + "net/url" + "path/filepath" + "regexp" + "strings" + "time" + "unicode" + "unicode/utf8" + + "github.com/google/uuid" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/infrarepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorycontentrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositoryrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorystoragenoderepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorytaskrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryNodePlatform" +) + +const ( + offlineAfter = 30 * time.Second + defaultPageSize = 50 + maximumPageSize = 200 + maximumLabels = 32 + maximumToolVersions = 64 + minimumPreflightInodes = int64(10000) +) + +var ( + ErrUnauthenticated = errors.New("authenticated user is required") + ErrForbidden = errors.New("user cannot manage repository storage nodes in this project") + ErrProjectChange = errors.New("repository storage node project cannot be changed") + ErrDeleteConfirmation = errors.New("repository storage node deletion requires explicit confirmation") + ErrStorageNodeInUse = errors.New("repository storage node is assigned to one or more repositories") + ErrInvalidHeartbeat = errors.New("repository storage node heartbeat is invalid") + ErrCertificateMismatch = errors.New("repository storage node certificate fingerprint does not match") + ErrPreflightDisabled = errors.New("repository task preflight is not configured") + storageNodeNamePattern = regexp.MustCompile(`^[A-Za-z0-9._-]+$`) + tlsServerNamePattern = regexp.MustCompile(`^[A-Za-z0-9.-]*$`) + nodeMetadataKeyPattern = regexp.MustCompile(`^[A-Za-z0-9._+-]+$`) + sha256Pattern = regexp.MustCompile(`^[0-9a-f]{64}$`) +) + +type projectReader interface { + DescribeProject(entity.Context, request.DescribeProjectRequest, entity.Session) (request.DescribeProjectResponse, error) +} + +type nodeRepositorySummaryReader interface { + ListNodeRepositorySummaries(string, entity.Session) ([]po.RepositoryNodeRepositorySummary, error) +} + +type nodeRepositoryUsageWriter interface { + ReplaceRepositoryUsages(string, []po.RepositoryNodeRepositoryUsage, entity.Session) error +} + +type Service = *service + +type service struct { + db entity.SessionFactory + nodes repositorystoragenoderepo.Repository + projects projectReader + content repositorycontentrepo.Repository + repositories repositoryrepo.Repository + lifecycle repositorystoragenoderepo.LifecycleRepository + tasks repositorytaskrepo.Repository + targets repositorytaskrepo.TargetRepository + issuer certificateIssuer + lifecyclePolicy request.RepositoryNodeLifecyclePolicy + artifactRoot string + policy request.RepositoryPreflightPolicy + now func() time.Time +} + +func New(db *database.DBClient) Service { + value := newService(db, repositorystoragenoderepo.New(db), infrarepo.NewInfrastructureRepository(db)) + value.content = repositorycontentrepo.New(db) + value.repositories = repositoryrepo.New(db) + value.lifecycle = repositorystoragenoderepo.NewLifecycleRepository(db) + value.tasks = repositorytaskrepo.New(db) + value.targets = repositorytaskrepo.NewTargetRepository(db) + return value +} + +func newService(db entity.SessionFactory, nodes repositorystoragenoderepo.Repository, projects projectReader) Service { + return &service{db: db, nodes: nodes, projects: projects, now: func() time.Time { return time.Now().UTC() }} +} + +func (s *service) SyncTables() error { + if err := s.nodes.SyncTables(); err != nil { + return err + } + if s.lifecycle != nil { + if err := s.lifecycle.SyncTables(); err != nil { + return err + } + } + if s.targets != nil { + return s.targets.SyncTables() + } + return nil +} + +func (s *service) ConfigurePreflight(policy request.RepositoryPreflightPolicy) { + if policy.SafetyPercent <= 0 { + policy.SafetyPercent = 20 + } + if policy.HardUsagePercent <= 0 { + policy.HardUsagePercent = 90 + } + if policy.TTL <= 0 { + policy.TTL = 5 * time.Minute + } + s.policy = policy +} + +func (s *service) Create(ctx entity.Context, spec request.RepositoryStorageNodeSpec) ( + value *po.RepositoryStorageNode, err error, +) { + spec, err = normalizeSpec(spec) + if err != nil { + return nil, err + } + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + if err = s.authorizeProject(ctx, spec.ProjectID, tx); err != nil { + return nil, err + } + labels, err := json.Marshal(spec.Labels) + if err != nil { + return nil, fmt.Errorf("encode storage node labels: %w", err) + } + status := request.RepositoryStorageNodeStatusUnknown + if !spec.Enabled { + status = request.RepositoryStorageNodeStatusDisabled + } + value = &po.RepositoryStorageNode{ + ID: uuid.NewString(), ProjectID: spec.ProjectID, Name: spec.Name, Description: spec.Description, + ControlBaseURL: spec.ControlBaseURL, PublishBaseURL: spec.PublishBaseURL, + TLSServerName: spec.TLSServerName, CertificateFingerprint: spec.CertificateFingerprint, + Status: status, Enabled: spec.Enabled, LabelsJSON: labels, CreatedBy: ctx.User.LoginName, + } + if err = s.nodes.CreateNode(value, tx); err != nil { + return nil, err + } + return value, nil +} + +func (s *service) Update(ctx entity.Context, id string, spec request.RepositoryStorageNodeSpec) ( + value *po.RepositoryStorageNode, err error, +) { + if err = validateUUID("storage_node_id", id); err != nil { + return nil, err + } + spec, err = normalizeSpec(spec) + if err != nil { + return nil, err + } + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + value, err = s.nodes.GetNodeByID(id, tx) + if err != nil { + return nil, err + } + if value.ProjectID != spec.ProjectID { + return nil, ErrProjectChange + } + if err = s.authorizeProject(ctx, value.ProjectID, tx); err != nil { + return nil, err + } + labels, err := json.Marshal(spec.Labels) + if err != nil { + return nil, fmt.Errorf("encode storage node labels: %w", err) + } + value.Name, value.Description = spec.Name, spec.Description + value.ControlBaseURL, value.PublishBaseURL = spec.ControlBaseURL, spec.PublishBaseURL + value.TLSServerName, value.CertificateFingerprint = spec.TLSServerName, spec.CertificateFingerprint + value.LabelsJSON, value.Enabled = labels, spec.Enabled + if !spec.Enabled { + value.Status = request.RepositoryStorageNodeStatusDisabled + if s.lifecycle != nil { + if err = s.lifecycle.RevokeCertificates(value.ID, s.now(), tx); err != nil { + return nil, err + } + } + } else if value.Status == request.RepositoryStorageNodeStatusDisabled { + value.Status = request.RepositoryStorageNodeStatusUnknown + } + if err = s.nodes.UpdateNode(value, tx); err != nil { + return nil, err + } + return s.nodes.GetNodeByID(id, tx) +} + +func (s *service) Describe(ctx entity.Context, id string) (*po.RepositoryStorageNode, error) { + if err := validateUUID("storage_node_id", id); err != nil { + return nil, err + } + value, err := s.nodes.GetNodeByID(id, nil) + if err != nil { + return nil, err + } + if err = s.authorizeProject(ctx, value.ProjectID, nil); err != nil { + return nil, err + } + if _, err = s.MarkOffline(); err != nil { + return nil, err + } + value, err = s.nodes.GetNodeByID(id, nil) + if err != nil { + return nil, err + } + if reader, ok := s.nodes.(nodeRepositorySummaryReader); ok { + value.Repositories, err = reader.ListNodeRepositorySummaries(value.ID, nil) + if err != nil { + return nil, err + } + value.RepositoryCount = int64(len(value.Repositories)) + value.RepositoryPhysicalBytes = 0 + for _, repository := range value.Repositories { + value.RepositoryPhysicalBytes += repository.PhysicalBytes + } + } + if s.content != nil { + value.MetadataSigningKeys, err = s.content.ListMetadataSigningKeys(value.ID, nil) + if err != nil { + return nil, err + } + } + return value, nil +} + +// DescribeInternal is reserved for the mTLS repository-node control plane. +func (s *service) DescribeInternal(id string) (*po.RepositoryStorageNode, error) { + if err := validateUUID("storage_node_id", id); err != nil { + return nil, err + } + if _, err := s.MarkOffline(); err != nil { + return nil, err + } + return s.nodes.GetNodeByID(id, nil) +} + +func (s *service) MetadataSigningKeyInternal(nodeID, keyRef string) (*po.RepositoryMetadataSigningKey, error) { + if err := validateUUID("storage_node_id", nodeID); err != nil { + return nil, err + } + keyRef = strings.TrimSpace(keyRef) + if keyRef == "" || len(keyRef) > 128 || !nodeMetadataKeyPattern.MatchString(keyRef) || s.content == nil { + return nil, errors.New("repository metadata signing key reference is invalid") + } + value, err := s.content.GetMetadataSigningKey(nodeID, keyRef, nil) + if err != nil { + return nil, err + } + if value == nil || value.Status != "AVAILABLE" { + return nil, errors.New("repository metadata signing key is unavailable on the storage node") + } + return value, nil +} + +// AuthenticateInternal binds a verified mTLS leaf certificate to the requested node record. +func (s *service) AuthenticateInternal(id, certificateFingerprint string) (*po.RepositoryStorageNode, error) { + if err := validateUUID("storage_node_id", id); err != nil { + return nil, err + } + fingerprint := strings.ToLower(strings.ReplaceAll(strings.TrimSpace(certificateFingerprint), ":", "")) + decoded, err := hex.DecodeString(fingerprint) + if err != nil || len(decoded) != sha256.Size { + return nil, ErrCertificateMismatch + } + node, err := s.nodes.GetNodeByID(id, nil) + if err != nil { + return nil, err + } + if !node.Enabled || node.Status == request.RepositoryStorageNodeStatusDisabled { + return nil, ErrCertificateMismatch + } + if s.lifecycle != nil { + count, countErr := s.lifecycle.CountCertificates(id, nil) + if countErr != nil { + return nil, countErr + } + if count > 0 { + if _, certErr := s.lifecycle.FindUsableCertificate(id, fingerprint, s.now(), nil); certErr != nil { + return nil, ErrCertificateMismatch + } + return node, nil + } + } + expected, err := hex.DecodeString(node.CertificateFingerprint) + if err != nil || subtle.ConstantTimeCompare(expected, decoded) != 1 { + return nil, ErrCertificateMismatch + } + return node, nil +} + +func (s *service) List(ctx entity.Context, filter request.ListRepositoryStorageNodesRequest) ( + []po.RepositoryStorageNode, request.PageResponse, error, +) { + values, page, _, err := s.ListWithStatistics(ctx, filter) + return values, page, err +} + +func (s *service) ListWithStatistics(ctx entity.Context, filter request.ListRepositoryStorageNodesRequest) ( + []po.RepositoryStorageNode, request.PageResponse, request.RepositoryStorageNodeStatistics, error, +) { + var statistics request.RepositoryStorageNodeStatistics + if filter.ProjectID <= 0 { + return nil, request.PageResponse{}, statistics, fmt.Errorf("project_id must be greater than zero") + } + if err := s.authorizeProject(ctx, filter.ProjectID, nil); err != nil { + return nil, request.PageResponse{}, statistics, err + } + filter.Status = strings.ToUpper(strings.TrimSpace(filter.Status)) + if filter.Status != "" && !isNodeStatus(filter.Status) { + return nil, request.PageResponse{}, statistics, + fmt.Errorf("unsupported repository storage node status %q", filter.Status) + } + filter.Keyword = strings.TrimSpace(filter.Keyword) + filter.PageRequest = normalizePage(filter.PageRequest) + // Refresh persisted liveness before filtering/paging and full-set statistics. + if _, err := s.MarkOffline(); err != nil { + return nil, request.PageResponse{}, statistics, err + } + values, page, err := s.nodes.ListNodes(filter, nil) + if err != nil { + return nil, request.PageResponse{}, statistics, err + } + statisticsRepository, ok := s.nodes.(repositorystoragenoderepo.StatisticsRepository) + if !ok { + return nil, request.PageResponse{}, statistics, + errors.New("repository storage node statistics are unavailable") + } + statistics, err = statisticsRepository.Statistics(filter, nil) + if err != nil { + return nil, request.PageResponse{}, statistics, err + } + return values, page, statistics, nil +} + +func (s *service) Delete(ctx entity.Context, id string, confirm bool) (err error) { + if !confirm { + return ErrDeleteConfirmation + } + if err = validateUUID("storage_node_id", id); err != nil { + return err + } + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + value, err := s.nodes.GetNodeByID(id, tx) + if err != nil { + return err + } + if err = s.authorizeProject(ctx, value.ProjectID, tx); err != nil { + return err + } + assigned, err := s.nodes.CountAssignedRepositories(id, tx) + if err != nil { + return err + } + if assigned > 0 { + return ErrStorageNodeInUse + } + if s.lifecycle != nil { + if err = s.lifecycle.RevokeCertificates(id, s.now(), tx); err != nil { + return err + } + if enrollment, findErr := s.lifecycle.FindLatestEnrollmentByNode(id, tx); findErr == nil { + if err = s.lifecycle.InvalidateBootstrapTokens(enrollment.ID, s.now(), tx); err != nil { + return err + } + } else if !errors.Is(findErr, repositorystoragenoderepo.ErrNodeEnrollmentNotFound) { + return findErr + } + } + return s.nodes.DeleteNode(id, tx) +} + +// RecordHeartbeat is called only by the mTLS-protected internal node endpoint. +func (s *service) RecordHeartbeat(input request.RepositoryStorageNodeHeartbeat) ( + value *po.RepositoryStorageNode, err error, +) { + architecture, filesystemType, tools, errorCode, errorMessage, err := normalizeNodeObservation(input) + if err != nil { + return nil, err + } + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + value, err = s.nodes.GetNodeByID(input.NodeID, tx) + if err != nil { + return nil, err + } + serverNow := s.now() + value.FilesystemType = filesystemType + value.Hostname, value.IPAddress = strings.TrimSpace(input.Hostname), strings.TrimSpace(input.IPAddress) + if architecture != "" { + value.Architecture = architecture + } + value.OSName, value.OSVersion = strings.TrimSpace(input.OSName), strings.TrimSpace(input.OSVersion) + value.AgentVersion = strings.TrimSpace(input.AgentVersion) + value.CapacityBytes, value.FreeBytes, value.FreeInodes = input.CapacityBytes, input.FreeBytes, input.FreeInodes + value.ToolVersionsJSON = tools + value.LastHeartbeatAt = timePointer(serverNow) + value.LastErrorCode, value.LastErrorMessage = errorCode, errorMessage + if value.Enabled { + value.Status = request.RepositoryStorageNodeStatusOnline + } else { + value.Status = request.RepositoryStorageNodeStatusDisabled + } + if err = s.nodes.UpdateNode(value, tx); err != nil { + return nil, err + } + if err = s.persistRepositoryUsages(input.RepositoryUsages, input.RepositoryUsageSnapshot, + value.ID, serverNow, tx); err != nil { + return nil, err + } + if err = s.persistMetadataSigningKeys(input.MetadataSigningKeys, input.MetadataKeySnapshot, + value.ID, serverNow, tx); err != nil { + return nil, err + } + if err = s.completeEnrollmentOnHeartbeat(value.ID, serverNow, tx); err != nil { + return nil, err + } + return value, nil +} + +// RecordProbe persists an active mTLS probe as both the newest node snapshot and +// a generic preflight record. Task-specific preflights remain separate records. +func (s *service) RecordProbe(ctx entity.Context, input request.RepositoryStorageNodeHeartbeat) ( + node *po.RepositoryStorageNode, value *po.RepositoryPreflightRecord, err error, +) { + if s.policy.TTL <= 0 { + return nil, nil, ErrPreflightDisabled + } + architecture, filesystemType, tools, errorCode, errorMessage, err := normalizeNodeObservation(input) + if err != nil { + return nil, nil, err + } + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + node, err = s.nodes.GetNodeByID(input.NodeID, tx) + if err != nil { + return nil, nil, err + } + if err = s.authorizeProject(ctx, node.ProjectID, tx); err != nil { + return nil, nil, err + } + now := s.now() + node.FilesystemType = filesystemType + node.Hostname, node.IPAddress = strings.TrimSpace(input.Hostname), strings.TrimSpace(input.IPAddress) + if architecture != "" { + node.Architecture = architecture + } + node.OSName, node.OSVersion = strings.TrimSpace(input.OSName), strings.TrimSpace(input.OSVersion) + node.AgentVersion = strings.TrimSpace(input.AgentVersion) + node.CapacityBytes, node.FreeBytes, node.FreeInodes = input.CapacityBytes, input.FreeBytes, input.FreeInodes + node.ToolVersionsJSON = tools + node.LastHeartbeatAt, node.LastProbeAt = timePointer(now), timePointer(now) + node.LastErrorCode, node.LastErrorMessage = errorCode, errorMessage + if node.Enabled { + node.Status = request.RepositoryStorageNodeStatusOnline + } else { + node.Status = request.RepositoryStorageNodeStatusDisabled + } + requiredFreeBytes := s.policy.MinimumFreeBytes + value = &po.RepositoryPreflightRecord{ID: uuid.NewString(), StorageNodeID: node.ID, + Status: request.RepositoryPreflightStatusPass, FilesystemType: filesystemType, + CapacityBytes: input.CapacityBytes, FreeBytes: input.FreeBytes, RequiredFreeBytes: requiredFreeBytes, + FreeInodes: input.FreeInodes, RequiredInodes: minimumPreflightInodes, + ToolVersionsJSON: append([]byte(nil), tools...), ExpiresAt: now.Add(s.policy.TTL)} + value.ErrorCode, value.ErrorMessage = s.evaluateNode(node, value, now) + if value.ErrorCode != "" { + value.Status = request.RepositoryPreflightStatusFail + } + if err = s.nodes.UpdateNode(node, tx); err != nil { + return nil, nil, err + } + if err = s.nodes.CreatePreflight(value, tx); err != nil { + return nil, nil, err + } + if err = s.persistRepositoryUsages(input.RepositoryUsages, input.RepositoryUsageSnapshot, + node.ID, now, tx); err != nil { + return nil, nil, err + } + return node, value, nil +} + +func (s *service) persistRepositoryUsages(input []request.RepositoryNodeRepositoryUsage, snapshot bool, + storageNodeID string, fallbackObservedAt time.Time, tx entity.Session, +) error { + if len(input) == 0 && !snapshot { + return nil + } + writer, ok := s.nodes.(nodeRepositoryUsageWriter) + if !ok { + return errors.New("repository node usage persistence is unavailable") + } + usages := make([]po.RepositoryNodeRepositoryUsage, 0, len(input)) + seen := make(map[string]struct{}, len(input)) + for _, usage := range input { + if err := validateUUID("repository_id", usage.RepositoryID); err != nil { + return err + } + if _, exists := seen[usage.RepositoryID]; exists || usage.LogicalBytes < 0 || usage.PhysicalBytes < 0 { + return ErrInvalidHeartbeat + } + seen[usage.RepositoryID] = struct{}{} + observedAt := usage.ObservedAt + if observedAt.IsZero() { + observedAt = fallbackObservedAt + } + usages = append(usages, po.RepositoryNodeRepositoryUsage{StorageNodeID: storageNodeID, + RepositoryID: usage.RepositoryID, LogicalBytes: usage.LogicalBytes, + PhysicalBytes: usage.PhysicalBytes, PublishStatus: strings.TrimSpace(usage.PublishStatus), + ObservedAt: observedAt.UTC()}) + } + return writer.ReplaceRepositoryUsages(storageNodeID, usages, tx) +} + +func (s *service) persistMetadataSigningKeys(input []request.RepositoryMetadataSigningKey, snapshot bool, + storageNodeID string, fallbackObservedAt time.Time, tx entity.Session, +) error { + if len(input) == 0 && !snapshot { + return nil + } + if s.content == nil { + return errors.New("repository metadata signing key persistence is unavailable") + } + values := make([]po.RepositoryMetadataSigningKey, 0, len(input)) + seen := make(map[string]struct{}, len(input)) + for _, key := range input { + keyRef := strings.TrimSpace(key.KeyRef) + fingerprint := strings.ToUpper(strings.TrimSpace(key.Fingerprint)) + name := strings.TrimSpace(key.PublicKeyName) + status := strings.ToUpper(strings.TrimSpace(key.Status)) + if keyRef == "" || len(keyRef) > 128 || !nodeMetadataKeyPattern.MatchString(keyRef) || + len(fingerprint) < 40 || len(fingerprint) > 64 || !isUpperHex(fingerprint) || + name == "" || len(name) > 255 || filepath.Base(name) != name || containsControl(name) || + (status != "AVAILABLE" && status != "INVALID" && status != "DISABLED") || + len(key.PublicKeyURL) > 2048 || containsControl(key.PublicKeyURL) { + return ErrInvalidHeartbeat + } + if _, duplicate := seen[keyRef]; duplicate { + return ErrInvalidHeartbeat + } + seen[keyRef] = struct{}{} + observedAt := key.ObservedAt + if observedAt.IsZero() { + observedAt = fallbackObservedAt + } + values = append(values, po.RepositoryMetadataSigningKey{ID: uuid.NewSHA1(uuid.MustParse(storageNodeID), + []byte(keyRef)).String(), StorageNodeID: storageNodeID, KeyRef: keyRef, Fingerprint: fingerprint, + PublicKeyName: name, PublicKeyURL: strings.TrimSpace(key.PublicKeyURL), Status: status, + KeyCreatedAt: key.KeyCreatedAt, ObservedAt: observedAt.UTC()}) + } + return s.content.ReplaceMetadataSigningKeys(storageNodeID, values, tx) +} + +func isUpperHex(value string) bool { + for _, char := range value { + if (char < '0' || char > '9') && (char < 'A' || char > 'F') { + return false + } + } + return true +} + +func normalizeNodeObservation(input request.RepositoryStorageNodeHeartbeat) ( + architecture, filesystemType string, tools []byte, errorCode, errorMessage string, err error, +) { + if err = validateUUID("storage_node_id", input.NodeID); err != nil { + return "", "", nil, "", "", err + } + if strings.TrimSpace(input.Architecture) != "" { + architecture, err = repositoryNodePlatform.NormalizeArchitecture(input.Architecture) + if err != nil { + return "", "", nil, "", "", fmt.Errorf("%w: architecture is invalid", ErrInvalidHeartbeat) + } + } + if input.CapacityBytes <= 0 || input.FreeBytes < 0 || input.FreeBytes > input.CapacityBytes || input.FreeInodes < 0 { + return "", "", nil, "", "", ErrInvalidHeartbeat + } + if len(input.ToolVersions) > maximumToolVersions { + return "", "", nil, "", "", fmt.Errorf("%w: tool_versions exceeds %d entries", ErrInvalidHeartbeat, maximumToolVersions) + } + filesystemType = strings.ToLower(strings.TrimSpace(input.FilesystemType)) + if filesystemType == "" || len(filesystemType) > 32 || !nodeMetadataKeyPattern.MatchString(filesystemType) { + return "", "", nil, "", "", fmt.Errorf("%w: filesystem_type is invalid", ErrInvalidHeartbeat) + } + for key, value := range input.ToolVersions { + if key == "" || len(key) > 64 || !nodeMetadataKeyPattern.MatchString(key) || + len(value) > 128 || containsControl(value) { + return "", "", nil, "", "", fmt.Errorf("%w: tool_versions contains invalid metadata", ErrInvalidHeartbeat) + } + } + errorCode = strings.TrimSpace(input.ErrorCode) + errorMessage = strings.TrimSpace(input.ErrorMessage) + if len(errorCode) > 64 || (errorCode != "" && !nodeMetadataKeyPattern.MatchString(errorCode)) || + utf8.RuneCountInString(errorMessage) > 2048 || containsControl(errorMessage) { + return "", "", nil, "", "", fmt.Errorf("%w: error detail is invalid", ErrInvalidHeartbeat) + } + tools, err = json.Marshal(input.ToolVersions) + if err != nil { + return "", "", nil, "", "", fmt.Errorf("encode tool versions: %w", err) + } + return architecture, filesystemType, tools, errorCode, errorMessage, nil +} + +func (s *service) MarkOffline() (int64, error) { + return s.nodes.MarkOfflineBefore(s.now().Add(-offlineAfter), nil) +} + +// MarkOfflineContext allows periodic maintenance to stop an in-flight query. +func (s *service) MarkOfflineContext(ctx context.Context) (int64, error) { + if err := ctx.Err(); err != nil { + return 0, err + } + return s.nodes.MarkOfflineBefore(s.now().Add(-offlineAfter), s.db.DB().WithContext(ctx)) +} + +// PrepareTaskPreflight calculates and persists the capacity gate in the same +// transaction as the task. Expected environmental failures are returned as a +// FAIL record so the rejected task and its evidence remain queryable. +func (s *service) PrepareTaskPreflight(_ entity.Context, repository *po.SoftwareRepository, taskID, + storageNodeID string, input request.EnqueueRepositoryTaskRequest, session entity.Session, +) (*po.RepositoryPreflightRecord, error) { + if s.content == nil || s.policy.TTL <= 0 { + return nil, ErrPreflightDisabled + } + if repository == nil || strings.TrimSpace(storageNodeID) == "" { + return nil, errors.New("repository storage node is required for preflight") + } + node, err := s.nodes.GetNodeByID(storageNodeID, session) + if err != nil { + return nil, err + } + estimate, err := s.estimateTask(repository, input, session) + if err != nil { + return nil, err + } + now := s.now() + value := &po.RepositoryPreflightRecord{ + ID: uuid.NewString(), StorageNodeID: node.ID, RepositoryID: repository.ID, TaskID: taskID, + Status: request.RepositoryPreflightStatusPass, FilesystemType: node.FilesystemType, + CapacityBytes: node.CapacityBytes, FreeBytes: node.FreeBytes, FreeInodes: node.FreeInodes, + RequiredFreeBytes: estimate.requiredFreeBytes, RequiredInodes: estimate.requiredInodes, + EstimatedPackageCount: estimate.packageCount, EstimatedImportBytes: estimate.importBytes, + ToolVersionsJSON: append([]byte(nil), node.ToolVersionsJSON...), ExpiresAt: now.Add(s.policy.TTL), + } + value.ErrorCode, value.ErrorMessage = s.evaluateNode(node, value, now) + if value.ErrorCode != "" { + value.Status = request.RepositoryPreflightStatusFail + } + if err = s.nodes.CreatePreflight(value, session); err != nil { + return nil, err + } + node.LastProbeAt = timePointer(now) + if err = s.nodes.UpdateNode(node, session); err != nil { + return nil, err + } + return value, nil +} + +// RevalidateTaskPreflight performs the lightweight claim-time check against +// the newest heartbeat. Expired passing records are refreshed from the same +// immutable estimate so queued tasks are not rejected only because they waited +// behind another repository mutation. +func (s *service) RevalidateTaskPreflight(task *po.RepositoryTask, session entity.Session) error { + if task == nil || task.PreflightID == "" { + return preflightError("REPOSITORY_NODE_PRECHECK_STALE", "task has no bound preflight record") + } + value, err := s.nodes.GetPreflightByID(task.PreflightID, session) + if err != nil { + return preflightError("REPOSITORY_NODE_PRECHECK_STALE", "task preflight record is unavailable") + } + if value.TaskID != task.ID || value.RepositoryID != task.RepositoryID || value.StorageNodeID != task.StorageNodeID { + return preflightError("REPOSITORY_NODE_PRECHECK_STALE", "task preflight identity does not match the task") + } + if value.Status != request.RepositoryPreflightStatusPass { + return preflightError(value.ErrorCode, value.ErrorMessage) + } + node, err := s.nodes.GetNodeByID(task.StorageNodeID, session) + if err != nil { + return err + } + now := s.now() + if !value.ExpiresAt.After(now) { + refreshed := &po.RepositoryPreflightRecord{ + ID: uuid.NewString(), StorageNodeID: value.StorageNodeID, RepositoryID: value.RepositoryID, + TaskID: value.TaskID, Status: request.RepositoryPreflightStatusPass, + FilesystemType: node.FilesystemType, CapacityBytes: node.CapacityBytes, + FreeBytes: node.FreeBytes, FreeInodes: node.FreeInodes, + RequiredFreeBytes: value.RequiredFreeBytes, RequiredInodes: value.RequiredInodes, + EstimatedPackageCount: value.EstimatedPackageCount, EstimatedImportBytes: value.EstimatedImportBytes, + ToolVersionsJSON: append([]byte(nil), node.ToolVersionsJSON...), ExpiresAt: now.Add(s.policy.TTL), + } + refreshed.ErrorCode, refreshed.ErrorMessage = s.evaluateNode(node, refreshed, now) + if refreshed.ErrorCode != "" { + refreshed.Status = request.RepositoryPreflightStatusFail + } + if err = s.nodes.CreatePreflight(refreshed, session); err != nil { + return err + } + task.PreflightID = refreshed.ID + value = refreshed + if value.Status != request.RepositoryPreflightStatusPass { + return preflightError(value.ErrorCode, value.ErrorMessage) + } + } + probe := *value + probe.FilesystemType = node.FilesystemType + probe.CapacityBytes, probe.FreeBytes, probe.FreeInodes = node.CapacityBytes, node.FreeBytes, node.FreeInodes + probe.ToolVersionsJSON = node.ToolVersionsJSON + if code, message := s.evaluateNode(node, &probe, now); code != "" { + return preflightError(code, message) + } + return nil +} + +type taskCapacityEstimate struct { + importBytes int64 + packageCount int64 + requiredFreeBytes int64 + requiredInodes int64 +} + +func (s *service) estimateTask(repository *po.SoftwareRepository, input request.EnqueueRepositoryTaskRequest, + session entity.Session, +) (taskCapacityEstimate, error) { + activeBytes, activePackages := int64(0), int64(0) + if repository.ActiveRevisionID != "" { + revision, err := s.content.GetRevision(repository.ID, repository.ActiveRevisionID, session) + if err != nil { + return taskCapacityEstimate{}, err + } + activeBytes, activePackages = revision.TotalBytes, int64(revision.PackageCount) + } + importBytes, projectedBytes, packageCount := int64(0), activeBytes, activePackages + switch input.Type { + case request.RepositoryTaskTypeSync: + // Until upstream metadata supplies an exact size, reserve the ceiling for + // the actual execution mode. An empty include list executes full reposync. + var snapshot request.RepositorySyncSnapshot + _ = json.Unmarshal([]byte(input.PayloadJSON), &snapshot) + importBytes, packageCount = s.syncLimits(len(snapshot.IncludePackages) > 0) + projectedBytes = importBytes + case request.RepositoryTaskTypeUpload: + var snapshot request.RepositoryUploadSnapshot + if err := json.Unmarshal([]byte(input.PayloadJSON), &snapshot); err != nil || snapshot.SizeBytes <= 0 { + return taskCapacityEstimate{}, errors.New("upload task has no usable size estimate") + } + importBytes, packageCount = snapshot.SizeBytes, 1 + if snapshot.Kind == request.RepositoryUploadKindTarGZ { + importBytes, packageCount = s.policy.MaxExpandedBytes, s.policy.MaxArchiveFiles + } + projectedBytes = checkedAdd(activeBytes, importBytes) + case request.RepositoryTaskTypeMigrateRepository: + var snapshot request.RepositoryMigrationSnapshot + if err := json.Unmarshal([]byte(input.PayloadJSON), &snapshot); err != nil || snapshot.TotalBytes < 0 { + return taskCapacityEstimate{}, errors.New("migration task has no usable size estimate") + } + importBytes, projectedBytes, packageCount = snapshot.TotalBytes, snapshot.TotalBytes, int64(snapshot.PackageCount) + case request.RepositoryTaskTypeDeletePackage, request.RepositoryTaskTypeRebuildMetadata, + request.RepositoryTaskTypeDeleteRepository, request.RepositoryTaskTypePublish: + projectedBytes = activeBytes + default: + return taskCapacityEstimate{}, nil + } + base := checkedAdd(activeBytes, importBytes, projectedBytes, s.policy.MetadataReserve) + required := checkedPercent(base, 100+s.policy.SafetyPercent) + if required < s.policy.MinimumFreeBytes { + required = s.policy.MinimumFreeBytes + } + requiredInodes := packageCount * 4 + if requiredInodes < minimumPreflightInodes { + requiredInodes = minimumPreflightInodes + } + return taskCapacityEstimate{importBytes: importBytes, packageCount: packageCount, + requiredFreeBytes: required, requiredInodes: requiredInodes}, nil +} + +func (s *service) syncLimits(filtered bool) (int64, int64) { + bytes, packages := s.policy.MaxSyncBytes, s.policy.MaxSyncPackages + if !filtered { + return bytes, packages + } + if s.policy.FilteredSyncBytes > 0 && s.policy.FilteredSyncBytes < bytes { + bytes = s.policy.FilteredSyncBytes + } + if s.policy.FilteredSyncPackages > 0 && s.policy.FilteredSyncPackages < packages { + packages = s.policy.FilteredSyncPackages + } + return bytes, packages +} + +func (s *service) evaluateNode(node *po.RepositoryStorageNode, value *po.RepositoryPreflightRecord, + now time.Time, +) (string, string) { + if !node.Enabled || node.Status != request.RepositoryStorageNodeStatusOnline || node.LastHeartbeatAt == nil || + now.Sub(node.LastHeartbeatAt.UTC()) > offlineAfter { + return "REPOSITORY_NODE_OFFLINE", "repository storage node has no fresh online heartbeat" + } + if node.LastErrorCode != "" { + return node.LastErrorCode, node.LastErrorMessage + } + switch strings.ToLower(value.FilesystemType) { + case "xfs", "ext4", "btrfs": + default: + return "REPOSITORY_NODE_FS_UNSUPPORTED", fmt.Sprintf("filesystem %q does not support the repository publication contract", value.FilesystemType) + } + tools := map[string]string{} + if err := json.Unmarshal(value.ToolVersionsJSON, &tools); err != nil { + return "REPOSITORY_NODE_TOOL_MISSING", "repository tool inventory is invalid" + } + for _, tool := range []string{"rpm", "rpmkeys", "dnf", "createrepo_c"} { + if strings.TrimSpace(tools[tool]) == "" { + return "REPOSITORY_NODE_TOOL_MISSING", "required repository tool is unavailable: " + tool + } + } + if value.FreeInodes < value.RequiredInodes { + return "REPOSITORY_NODE_INODE_EXHAUSTED", fmt.Sprintf("free_inodes=%d required_inodes=%d", value.FreeInodes, value.RequiredInodes) + } + if value.CapacityBytes <= 0 || value.FreeBytes < value.RequiredFreeBytes { + return "REPOSITORY_NODE_INSUFFICIENT_SPACE", fmt.Sprintf("free_bytes=%d required_free_bytes=%d", value.FreeBytes, value.RequiredFreeBytes) + } + remaining := value.FreeBytes - value.RequiredFreeBytes + minimumRemaining := checkedPercent(value.CapacityBytes, 100-s.policy.HardUsagePercent) + if remaining < minimumRemaining { + return "REPOSITORY_NODE_INSUFFICIENT_SPACE", fmt.Sprintf( + "projected_free_bytes=%d hard_threshold_free_bytes=%d", remaining, minimumRemaining) + } + return "", "" +} + +type codedPreflightError struct{ code, message string } + +func (e codedPreflightError) Error() string { return e.code + ": " + e.message } +func (e codedPreflightError) Code() string { return e.code } + +func preflightError(code, message string) error { + if strings.TrimSpace(code) == "" { + code = "REPOSITORY_NODE_PRECHECK_STALE" + } + return codedPreflightError{code: code, message: strings.TrimSpace(message)} +} + +func checkedAdd(values ...int64) int64 { + result := int64(0) + for _, value := range values { + if value < 0 || result > math.MaxInt64-value { + return math.MaxInt64 + } + result += value + } + return result +} + +func checkedPercent(value, percent int64) int64 { + if value <= 0 || percent <= 0 { + return 0 + } + if value > math.MaxInt64/percent { + return math.MaxInt64 + } + return (value*percent + 99) / 100 +} + +func (s *service) CreatePreflight(ctx entity.Context, input request.RepositoryPreflightResult) ( + value *po.RepositoryPreflightRecord, err error, +) { + if err = validateUUID("storage_node_id", input.StorageNodeID); err != nil { + return nil, err + } + if input.RepositoryID != "" { + if err = validateUUID("repository_id", input.RepositoryID); err != nil { + return nil, err + } + } + if input.Status != request.RepositoryPreflightStatusPass && input.Status != request.RepositoryPreflightStatusFail { + return nil, fmt.Errorf("unsupported repository preflight status %q", input.Status) + } + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + node, err := s.nodes.GetNodeByID(input.StorageNodeID, tx) + if err != nil { + return nil, err + } + if err = s.authorizeProject(ctx, node.ProjectID, tx); err != nil { + return nil, err + } + tools, err := json.Marshal(input.ToolVersions) + if err != nil { + return nil, fmt.Errorf("encode preflight tool versions: %w", err) + } + value = &po.RepositoryPreflightRecord{ + ID: uuid.NewString(), StorageNodeID: input.StorageNodeID, RepositoryID: input.RepositoryID, + TaskID: input.TaskID, Status: input.Status, FilesystemType: input.FilesystemType, + CapacityBytes: input.CapacityBytes, FreeBytes: input.FreeBytes, RequiredFreeBytes: input.RequiredFreeBytes, + FreeInodes: input.FreeInodes, RequiredInodes: input.RequiredInodes, + EstimatedPackageCount: input.EstimatedPackageCount, EstimatedImportBytes: input.EstimatedImportBytes, + ToolVersionsJSON: tools, ErrorCode: input.ErrorCode, ErrorMessage: input.ErrorMessage, + ExpiresAt: input.ExpiresAt.UTC(), + } + if value.ExpiresAt.IsZero() { + value.ExpiresAt = s.now().Add(5 * time.Minute) + } + if err = s.nodes.CreatePreflight(value, tx); err != nil { + return nil, err + } + return value, nil +} + +func (s *service) ListPreflights(ctx entity.Context, filter request.ListRepositoryPreflightRecordsRequest) ( + []po.RepositoryPreflightRecord, request.PageResponse, error, +) { + if err := validateUUID("storage_node_id", filter.StorageNodeID); err != nil { + return nil, request.PageResponse{}, err + } + node, err := s.nodes.GetNodeByID(filter.StorageNodeID, nil) + if err != nil { + return nil, request.PageResponse{}, err + } + if err = s.authorizeProject(ctx, node.ProjectID, nil); err != nil { + return nil, request.PageResponse{}, err + } + if filter.RepositoryID != "" { + if err = validateUUID("repository_id", filter.RepositoryID); err != nil { + return nil, request.PageResponse{}, err + } + } + filter.PageRequest = normalizePage(filter.PageRequest) + return s.nodes.ListPreflights(filter, nil) +} + +func (s *service) ImportTaskLogIndex(task *po.RepositoryTask, metadata request.RepositoryTaskLogMetadata, + session entity.Session, +) (*po.RepositoryTaskLogIndex, error) { + if task == nil || metadata.TaskID != task.ID || task.StorageNodeID == "" || + metadata.RelativePath != filepath.ToSlash(filepath.Join("task-logs", task.ID, "execution.jsonl.zst")) { + return nil, errors.New("repository task log identity is invalid") + } + if !sha256Pattern.MatchString(metadata.SHA256) || + (metadata.ErrorFingerprint != "" && !sha256Pattern.MatchString(metadata.ErrorFingerprint)) || + metadata.CompressedBytes <= 0 || metadata.UncompressedBytes <= 0 || metadata.LineCount <= 0 || + len(metadata.Summary) > 64<<10 || !utf8.ValidString(metadata.Summary) || + strings.Contains(metadata.Summary, "PGP PRIVATE KEY") { + return nil, errors.New("repository task log metadata is invalid") + } + retention := 14 * 24 * time.Hour + if metadata.CompletionStatus != request.RepositoryTaskStatusSuccess { + retention = 30 * 24 * time.Hour + } + if metadata.CreatedAt.IsZero() || metadata.ExpiresAt.IsZero() || + metadata.ExpiresAt.Sub(metadata.CreatedAt) != retention { + return nil, errors.New("repository task log retention is invalid") + } + value := &po.RepositoryTaskLogIndex{ + ID: uuid.NewSHA1(uuid.MustParse(task.ID), []byte("repository-task-log")).String(), + TaskID: task.ID, StorageNodeID: task.StorageNodeID, RelativePath: metadata.RelativePath, + SHA256: metadata.SHA256, CompressedBytes: metadata.CompressedBytes, + UncompressedBytes: metadata.UncompressedBytes, LineCount: metadata.LineCount, + Summary: metadata.Summary, ErrorFingerprint: metadata.ErrorFingerprint, + ExpiresAt: s.now().Add(retention), + } + if err := s.nodes.UpsertTaskLogIndex(value, session); err != nil { + return nil, err + } + return value, nil +} + +func (s *service) GetTaskLogIndexInternal(taskID string) (*po.RepositoryTaskLogIndex, error) { + if err := validateUUID("task_id", taskID); err != nil { + return nil, err + } + return s.nodes.GetTaskLogIndex(taskID, nil) +} + +func (s *service) authorizeProject(ctx entity.Context, projectID int64, session entity.Session) error { + if ctx.User == nil || strings.TrimSpace(ctx.User.LoginName) == "" { + return ErrUnauthenticated + } + project, err := s.projects.DescribeProject(ctx, request.DescribeProjectRequest{ID: projectID}, session) + if err != nil { + return fmt.Errorf("project %d not found: %w", projectID, err) + } + for _, role := range ctx.User.Roles { + if role.RoleId == request.DefaultGlobalRoleName { + return nil + } + } + if project.Project.Creator != ctx.User.LoginName { + return ErrForbidden + } + return nil +} + +func normalizeSpec(spec request.RepositoryStorageNodeSpec) (request.RepositoryStorageNodeSpec, error) { + if spec.ProjectID <= 0 { + return spec, fmt.Errorf("project_id must be greater than zero") + } + spec.Name = strings.TrimSpace(spec.Name) + if spec.Name == "" || utf8.RuneCountInString(spec.Name) > 128 || !storageNodeNamePattern.MatchString(spec.Name) { + return spec, fmt.Errorf("name must use 1-128 ASCII letters, digits, '.', '_' or '-'") + } + spec.Description = strings.TrimSpace(spec.Description) + if utf8.RuneCountInString(spec.Description) > 1024 || containsControl(spec.Description) { + return spec, fmt.Errorf("description is invalid or exceeds 1024 characters") + } + var err error + if spec.ControlBaseURL, err = normalizeBaseURL("control_base_url", spec.ControlBaseURL, true); err != nil { + return spec, err + } + if spec.PublishBaseURL, err = normalizeBaseURL("publish_base_url", spec.PublishBaseURL, false); err != nil { + return spec, err + } + spec.TLSServerName = strings.TrimSpace(spec.TLSServerName) + if len(spec.TLSServerName) > 255 || !tlsServerNamePattern.MatchString(spec.TLSServerName) { + return spec, fmt.Errorf("tls_server_name is invalid") + } + spec.CertificateFingerprint = strings.ToLower(strings.ReplaceAll(strings.TrimSpace(spec.CertificateFingerprint), ":", "")) + decoded, err := hex.DecodeString(spec.CertificateFingerprint) + if err != nil || len(decoded) != 32 { + return spec, fmt.Errorf("certificate_fingerprint must be a SHA-256 fingerprint") + } + if spec.Labels == nil { + spec.Labels = map[string]string{} + } + if len(spec.Labels) > maximumLabels { + return spec, fmt.Errorf("labels cannot exceed %d entries", maximumLabels) + } + for key, value := range spec.Labels { + if strings.TrimSpace(key) == "" || len(key) > 64 || len(value) > 256 || containsControl(key) || containsControl(value) { + return spec, fmt.Errorf("storage node label is invalid") + } + } + return spec, nil +} + +func normalizeBaseURL(field, raw string, requireHTTPS bool) (string, error) { + raw = strings.TrimSpace(raw) + if raw == "" || len(raw) > 2048 { + return "", fmt.Errorf("%s is required and cannot exceed 2048 characters", field) + } + parsed, err := url.Parse(raw) + if err != nil || parsed.Host == "" || parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" { + return "", fmt.Errorf("%s must be an absolute URL without credentials, query, or fragment", field) + } + if requireHTTPS && parsed.Scheme != "https" { + return "", fmt.Errorf("%s must use https", field) + } + if !requireHTTPS && parsed.Scheme != "http" && parsed.Scheme != "https" { + return "", fmt.Errorf("%s must use http or https", field) + } + parsed.Path = strings.TrimRight(parsed.Path, "/") + return strings.TrimRight(parsed.String(), "/"), nil +} + +func normalizePage(page request.PageRequest) request.PageRequest { + if page.PageNumber <= 0 { + page.PageNumber = 1 + } + if page.PageSize <= 0 { + page.PageSize = defaultPageSize + } + if page.PageSize > maximumPageSize { + page.PageSize = maximumPageSize + } + return page +} + +func isNodeStatus(value string) bool { + switch value { + case request.RepositoryStorageNodeStatusUnknown, request.RepositoryStorageNodeStatusOnline, + request.RepositoryStorageNodeStatusOffline, request.RepositoryStorageNodeStatusDisabled, + request.RepositoryStorageNodeStatusPending, request.RepositoryStorageNodeStatusInstalling, + request.RepositoryStorageNodeStatusInstallFailed: + return true + default: + return false + } +} + +func validateUUID(field, value string) error { + if _, err := uuid.Parse(strings.TrimSpace(value)); err != nil { + return fmt.Errorf("%s must be a UUID", field) + } + return nil +} + +func containsControl(value string) bool { + return strings.IndexFunc(value, unicode.IsControl) >= 0 +} + +func timePointer(value time.Time) *time.Time { return &value } diff --git a/manager/backend/services/footstone/domain/repositoryNodeService/service_test.go b/manager/backend/services/footstone/domain/repositoryNodeService/service_test.go new file mode 100644 index 0000000000000000000000000000000000000000..9e4d6b75c673ced2f79d78589d0a48f7704fb4f7 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryNodeService/service_test.go @@ -0,0 +1,526 @@ +package repositoryNodeService + +import ( + "database/sql" + "encoding/json" + "errors" + "strings" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + ftpb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/footstone" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorystoragenoderepo" +) + +const testNodeID = "33333333-3333-4333-8333-333333333333" + +func TestNormalizeSpec(t *testing.T) { + spec, err := normalizeSpec(validNodeSpec()) + if err != nil { + t.Fatal(err) + } + if spec.Name != "repo-node-a" || spec.ControlBaseURL != "https://node.example:19443" || + spec.PublishBaseURL != "http://node.example:18090" || len(spec.CertificateFingerprint) != 64 { + t.Fatalf("normalized spec = %#v", spec) + } +} + +func TestNormalizeSpecRejectsUnsafeControlURL(t *testing.T) { + spec := validNodeSpec() + spec.ControlBaseURL = "https://user:password@node.example:19443" + if _, err := normalizeSpec(spec); err == nil { + t.Fatal("normalizeSpec() error = nil") + } +} + +func TestNormalizeNodeObservationCanonicalizesArchitecture(t *testing.T) { + t.Parallel() + + input := request.RepositoryStorageNodeHeartbeat{ + NodeID: "11111111-1111-4111-8111-111111111111", + Architecture: " AMD64 ", + FilesystemType: "xfs", + CapacityBytes: 1024, + FreeBytes: 512, + FreeInodes: 10000, + ToolVersions: map[string]string{"rpm": "4.16"}, + } + architecture, filesystemType, _, _, _, err := normalizeNodeObservation(input) + if err != nil { + t.Fatalf("normalizeNodeObservation() error = %v", err) + } + if architecture != "x86_64" || filesystemType != "xfs" { + t.Fatalf("normalizeNodeObservation() = architecture %q, filesystem %q", architecture, filesystemType) + } +} + +func TestCreateAndHeartbeat(t *testing.T) { + repository := &fakeNodeRepository{} + service, mock, sqlDB := newNodeTestService(t, repository) + defer sqlDB.Close() + serverNow := time.Date(2026, 8, 28, 15, 30, 0, 0, time.UTC) + service.now = func() time.Time { return serverNow } + mock.ExpectBegin() + mock.ExpectCommit() + created, err := service.Create(globalNodeContext(), validNodeSpec()) + if err != nil { + t.Fatal(err) + } + if created.Status != request.RepositoryStorageNodeStatusUnknown || repository.node == nil { + t.Fatalf("created node = %#v", created) + } + created.ID = testNodeID + repository.node = created + mock.ExpectBegin() + mock.ExpectCommit() + heartbeat, err := service.RecordHeartbeat(request.RepositoryStorageNodeHeartbeat{ + NodeID: testNodeID, FilesystemType: "xfs", CapacityBytes: 1000, FreeBytes: 800, + FreeInodes: 20000, ToolVersions: map[string]string{"createrepo_c": "0.20.1"}, + ObservedAt: serverNow.Add(24 * time.Hour), + }) + if err != nil { + t.Fatal(err) + } + if heartbeat.Status != request.RepositoryStorageNodeStatusOnline || heartbeat.FreeBytes != 800 || + heartbeat.LastHeartbeatAt == nil || !heartbeat.LastHeartbeatAt.Equal(serverNow) { + t.Fatalf("heartbeat node = %#v", heartbeat) + } + if err = mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } +} + +func TestMarkOfflineUsesServerClock(t *testing.T) { + repository := &fakeNodeRepository{offlineCount: 2} + service, mock, sqlDB := newNodeTestService(t, repository) + defer sqlDB.Close() + serverNow := time.Date(2026, 8, 28, 16, 0, 0, 0, time.UTC) + service.now = func() time.Time { return serverNow } + count, err := service.MarkOffline() + if err != nil || count != 2 { + t.Fatalf("MarkOffline() count/error = %d/%v", count, err) + } + want := serverNow.Add(-30 * time.Second) + if !repository.offlineCutoff.Equal(want) { + t.Fatalf("offline cutoff = %s, want %s", repository.offlineCutoff, want) + } + if err = mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } +} + +func TestRecordProbeCreatesFreshPassingPreflight(t *testing.T) { + now := time.Date(2026, 9, 1, 14, 30, 0, 0, time.UTC) + repository := &fakeNodeRepository{node: &po.RepositoryStorageNode{ + ID: testNodeID, ProjectID: 42, Enabled: true, Status: request.RepositoryStorageNodeStatusOffline, + }} + service, mock, sqlDB := newNodeTestService(t, repository) + defer sqlDB.Close() + service.now = func() time.Time { return now } + service.ConfigurePreflight(request.RepositoryPreflightPolicy{ + MinimumFreeBytes: 100, HardUsagePercent: 90, TTL: 5 * time.Minute, + }) + mock.ExpectBegin() + mock.ExpectCommit() + repositoryID := "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" + node, preflight, err := service.RecordProbe(globalNodeContext(), request.RepositoryStorageNodeHeartbeat{ + NodeID: testNodeID, FilesystemType: "XFS", CapacityBytes: 1000, FreeBytes: 500, + FreeInodes: 20000, ToolVersions: requiredNodeTools(), ObservedAt: now.Add(-time.Second), + RepositoryUsages: []request.RepositoryNodeRepositoryUsage{{RepositoryID: repositoryID, + LogicalBytes: 128, PhysicalBytes: 256, PublishStatus: "READY", ObservedAt: now.Add(-time.Second)}}, + RepositoryUsageSnapshot: true, + }) + if err != nil { + t.Fatal(err) + } + if node.Status != request.RepositoryStorageNodeStatusOnline || node.LastProbeAt == nil || + !node.LastProbeAt.Equal(now) || node.LastHeartbeatAt == nil || !node.LastHeartbeatAt.Equal(now) { + t.Fatalf("probe node = %#v", node) + } + if preflight.Status != request.RepositoryPreflightStatusPass || preflight.ErrorCode != "" || + preflight.RequiredFreeBytes != 100 || preflight.RequiredInodes != minimumPreflightInodes || + preflight.RepositoryID != "" || preflight.TaskID != "" || repository.preflight != preflight { + t.Fatalf("probe preflight = %#v", preflight) + } + if len(repository.usages) != 1 || repository.usages[0].RepositoryID != repositoryID || + repository.usages[0].PhysicalBytes != 256 { + t.Fatalf("probe repository usages = %#v", repository.usages) + } + if err = mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } +} + +func TestRecordProbePersistsToolFailure(t *testing.T) { + now := time.Date(2026, 9, 1, 14, 35, 0, 0, time.UTC) + repository := &fakeNodeRepository{node: &po.RepositoryStorageNode{ + ID: testNodeID, ProjectID: 42, Enabled: true, Status: request.RepositoryStorageNodeStatusOnline, + }} + service, mock, sqlDB := newNodeTestService(t, repository) + defer sqlDB.Close() + service.now = func() time.Time { return now } + service.ConfigurePreflight(request.RepositoryPreflightPolicy{HardUsagePercent: 90, TTL: 5 * time.Minute}) + mock.ExpectBegin() + mock.ExpectCommit() + _, preflight, err := service.RecordProbe(globalNodeContext(), request.RepositoryStorageNodeHeartbeat{ + NodeID: testNodeID, FilesystemType: "xfs", CapacityBytes: 1000, FreeBytes: 500, + FreeInodes: 20000, ToolVersions: map[string]string{"rpm": "4.16"}, ObservedAt: now, + }) + if err != nil { + t.Fatal(err) + } + if preflight.Status != request.RepositoryPreflightStatusFail || + preflight.ErrorCode != "REPOSITORY_NODE_TOOL_MISSING" { + t.Fatalf("probe failure = %#v", preflight) + } + if err = mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } +} + +func requiredNodeTools() map[string]string { + return map[string]string{ + "rpm": "4.16", "rpmkeys": "4.16", "dnf": "4.14", "createrepo_c": "0.20", + } +} + +func TestPreflightCapacityFormulaUsesFullSyncLimitWithoutIncludes(t *testing.T) { + service, _, sqlDB := newNodeTestService(t, &fakeNodeRepository{}) + defer sqlDB.Close() + service.ConfigurePreflight(request.RepositoryPreflightPolicy{ + MaxExpandedBytes: 200, MaxSyncBytes: 100, MaxSyncPackages: 2000, + MaxArchiveFiles: 50, MetadataReserve: 10, MinimumFreeBytes: 1, + SafetyPercent: 20, HardUsagePercent: 90, TTL: 5 * time.Minute, + }) + estimate, err := service.estimateTask(&po.SoftwareRepository{}, request.EnqueueRepositoryTaskRequest{ + Type: request.RepositoryTaskTypeSync, PayloadJSON: `{}`, + }, nil) + if err != nil { + t.Fatal(err) + } + // (active 0 + import 100 + projected 100 + metadata 10) * 120%. + if estimate.importBytes != 100 || estimate.requiredFreeBytes != 252 || + estimate.packageCount != 2000 || estimate.requiredInodes != 10000 { + t.Fatalf("sync estimate = %#v", estimate) + } +} + +func TestPreflightCapacityFormulaUsesFilteredSyncLimitWithIncludes(t *testing.T) { + service, _, sqlDB := newNodeTestService(t, &fakeNodeRepository{}) + defer sqlDB.Close() + service.ConfigurePreflight(request.RepositoryPreflightPolicy{ + MaxSyncBytes: 100, MaxSyncPackages: 2000, + FilteredSyncBytes: 40, FilteredSyncPackages: 5, + MetadataReserve: 10, MinimumFreeBytes: 1, + SafetyPercent: 20, HardUsagePercent: 90, TTL: 5 * time.Minute, + }) + estimate, err := service.estimateTask(&po.SoftwareRepository{}, request.EnqueueRepositoryTaskRequest{ + Type: request.RepositoryTaskTypeSync, PayloadJSON: `{"include_packages":["curl"]}`, + }, nil) + if err != nil { + t.Fatal(err) + } + // (active 0 + import 40 + projected 40 + metadata 10) * 120%. + if estimate.importBytes != 40 || estimate.requiredFreeBytes != 108 || + estimate.packageCount != 5 || estimate.requiredInodes != 10000 { + t.Fatalf("filtered sync estimate = %#v", estimate) + } +} + +func TestPreflightCapacityFormulaAccountsForTarExpansion(t *testing.T) { + service, _, sqlDB := newNodeTestService(t, &fakeNodeRepository{}) + defer sqlDB.Close() + service.ConfigurePreflight(request.RepositoryPreflightPolicy{ + MaxExpandedBytes: 200, MaxSyncBytes: 100, MaxSyncPackages: 2000, + MaxArchiveFiles: 3000, MetadataReserve: 10, MinimumFreeBytes: 1, + SafetyPercent: 20, HardUsagePercent: 90, TTL: 5 * time.Minute, + }) + estimate, err := service.estimateTask(&po.SoftwareRepository{}, request.EnqueueRepositoryTaskRequest{ + Type: request.RepositoryTaskTypeUpload, + PayloadJSON: `{"kind":"TAR_GZ","size_bytes":20}`, + }, nil) + if err != nil { + t.Fatal(err) + } + if estimate.importBytes != 200 || estimate.requiredFreeBytes != 492 || + estimate.packageCount != 3000 || estimate.requiredInodes != 12000 { + t.Fatalf("tar estimate = %#v", estimate) + } +} + +func TestPreflightNodeGates(t *testing.T) { + now := time.Date(2026, 8, 31, 10, 0, 0, 0, time.UTC) + tools, _ := json.Marshal(map[string]string{ + "rpm": "4.16", "rpmkeys": "4.16", "dnf": "4.14", "createrepo_c": "0.20", + }) + node := &po.RepositoryStorageNode{ID: testNodeID, Enabled: true, + Status: request.RepositoryStorageNodeStatusOnline, FilesystemType: "xfs", + CapacityBytes: 1000, FreeBytes: 500, FreeInodes: 20000, + ToolVersionsJSON: tools, LastHeartbeatAt: &now, + } + service, _, sqlDB := newNodeTestService(t, &fakeNodeRepository{node: node}) + defer sqlDB.Close() + service.now = func() time.Time { return now } + service.ConfigurePreflight(request.RepositoryPreflightPolicy{SafetyPercent: 20, + HardUsagePercent: 90, TTL: 5 * time.Minute}) + base := &po.RepositoryPreflightRecord{FilesystemType: "xfs", CapacityBytes: 1000, + FreeBytes: 500, RequiredFreeBytes: 300, FreeInodes: 20000, RequiredInodes: 10000, + ToolVersionsJSON: tools} + if code, message := service.evaluateNode(node, base, now); code != "" { + t.Fatalf("PASS gate = %s/%s", code, message) + } + tests := []struct { + name string + edit func(*po.RepositoryStorageNode, *po.RepositoryPreflightRecord) + code string + }{ + {"space", func(_ *po.RepositoryStorageNode, value *po.RepositoryPreflightRecord) { value.RequiredFreeBytes = 450 }, "REPOSITORY_NODE_INSUFFICIENT_SPACE"}, + {"inode", func(_ *po.RepositoryStorageNode, value *po.RepositoryPreflightRecord) { value.RequiredInodes = 30000 }, "REPOSITORY_NODE_INODE_EXHAUSTED"}, + {"filesystem", func(_ *po.RepositoryStorageNode, value *po.RepositoryPreflightRecord) { value.FilesystemType = "tmpfs" }, "REPOSITORY_NODE_FS_UNSUPPORTED"}, + {"tool", func(_ *po.RepositoryStorageNode, value *po.RepositoryPreflightRecord) { + value.ToolVersionsJSON = []byte(`{"rpm":"4.16"}`) + }, "REPOSITORY_NODE_TOOL_MISSING"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + value := *base + test.edit(node, &value) + if code, _ := service.evaluateNode(node, &value, now); code != test.code { + t.Fatalf("gate code = %s, want %s", code, test.code) + } + }) + } +} + +func TestRevalidateTaskPreflightRefreshesExpiredPassingRecord(t *testing.T) { + now := time.Date(2026, 8, 31, 10, 0, 0, 0, time.UTC) + expires := now.Add(-time.Second) + tools := []byte(`{"rpm":"4","rpmkeys":"4","dnf":"4","createrepo_c":"0.20"}`) + repository := &fakeNodeRepository{node: &po.RepositoryStorageNode{ + ID: testNodeID, Enabled: true, Status: request.RepositoryStorageNodeStatusOnline, LastHeartbeatAt: &now, + FilesystemType: "xfs", CapacityBytes: 100000, FreeBytes: 80000, FreeInodes: 20000, + ToolVersionsJSON: tools, + }, preflight: &po.RepositoryPreflightRecord{ + ID: "55555555-5555-4555-8555-555555555555", TaskID: "66666666-6666-4666-8666-666666666666", + RepositoryID: "77777777-7777-4777-8777-777777777777", StorageNodeID: testNodeID, + Status: request.RepositoryPreflightStatusPass, ExpiresAt: expires, + RequiredFreeBytes: 10000, RequiredInodes: 10000, + }} + service, _, sqlDB := newNodeTestService(t, repository) + defer sqlDB.Close() + service.now = func() time.Time { return now } + service.ConfigurePreflight(request.RepositoryPreflightPolicy{TTL: 5 * time.Minute}) + task := &po.RepositoryTask{ + ID: repository.preflight.TaskID, RepositoryID: repository.preflight.RepositoryID, + StorageNodeID: testNodeID, PreflightID: repository.preflight.ID, + } + oldPreflightID := task.PreflightID + err := service.RevalidateTaskPreflight(task, nil) + if err != nil { + t.Fatalf("RevalidateTaskPreflight() error = %v", err) + } + if task.PreflightID == oldPreflightID || task.PreflightID != repository.preflight.ID { + t.Fatalf("refreshed preflight ID = %q, old = %q", task.PreflightID, oldPreflightID) + } + if !repository.preflight.ExpiresAt.Equal(now.Add(5*time.Minute)) || + repository.preflight.RequiredFreeBytes != 10000 || repository.preflight.RequiredInodes != 10000 { + t.Fatalf("refreshed preflight = %#v", repository.preflight) + } +} + +func TestImportTaskLogIndexKeepsOnlyBoundedMetadata(t *testing.T) { + repository := &fakeNodeRepository{} + service, _, sqlDB := newNodeTestService(t, repository) + defer sqlDB.Close() + now := time.Date(2026, 8, 31, 12, 0, 0, 0, time.UTC) + service.now = func() time.Time { return now } + task := &po.RepositoryTask{ID: "99999999-9999-4999-8999-999999999999", + StorageNodeID: testNodeID} + metadata := request.RepositoryTaskLogMetadata{TaskID: task.ID, + RelativePath: "task-logs/" + task.ID + "/execution.jsonl.zst", + SHA256: strings.Repeat("a", 64), CompressedBytes: 100, UncompressedBytes: 1000, + LineCount: 10, Summary: "download failed", ErrorFingerprint: strings.Repeat("b", 64), + CompletionStatus: request.RepositoryTaskStatusFailed, CreatedAt: now, ExpiresAt: now.Add(30 * 24 * time.Hour)} + value, err := service.ImportTaskLogIndex(task, metadata, nil) + if err != nil || value.TaskID != task.ID || value.StorageNodeID != testNodeID || + value.ExpiresAt.Sub(now) != 30*24*time.Hour || repository.taskLog == nil { + t.Fatalf("ImportTaskLogIndex() = %#v/%v", value, err) + } + metadata.Summary = strings.Repeat("x", (64<<10)+1) + if _, err = service.ImportTaskLogIndex(task, metadata, nil); err == nil { + t.Fatal("oversized task log summary was accepted") + } +} + +func TestDeleteRejectsAssignedNode(t *testing.T) { + repository := &fakeNodeRepository{node: &po.RepositoryStorageNode{ + ID: testNodeID, ProjectID: 42, Enabled: true, Status: request.RepositoryStorageNodeStatusOnline, + }, assigned: 1} + service, mock, sqlDB := newNodeTestService(t, repository) + defer sqlDB.Close() + mock.ExpectBegin() + mock.ExpectRollback() + err := service.Delete(globalNodeContext(), testNodeID, true) + if !errors.Is(err, ErrStorageNodeInUse) { + t.Fatalf("Delete() error = %v", err) + } + if err = mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } +} + +func TestAuthenticateInternalBindsCertificateFingerprint(t *testing.T) { + repository := &fakeNodeRepository{node: &po.RepositoryStorageNode{ + ID: testNodeID, ProjectID: 42, Enabled: true, + CertificateFingerprint: strings.Repeat("ab", 32), + }} + service, mock, sqlDB := newNodeTestService(t, repository) + defer sqlDB.Close() + fingerprint := strings.ToUpper(strings.TrimSuffix(strings.Repeat("ab:", 32), ":")) + value, err := service.AuthenticateInternal(testNodeID, fingerprint) + if err != nil || value.ID != testNodeID { + t.Fatalf("AuthenticateInternal() node/error = %#v/%v", value, err) + } + if _, err = service.AuthenticateInternal(testNodeID, strings.Repeat("cd", 32)); !errors.Is(err, ErrCertificateMismatch) { + t.Fatalf("AuthenticateInternal(mismatch) error = %v", err) + } + if err = mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } +} + +func validNodeSpec() request.RepositoryStorageNodeSpec { + return request.RepositoryStorageNodeSpec{ + ProjectID: 42, Name: " repo-node-a ", Description: " OC9 repository node ", + ControlBaseURL: "https://node.example:19443/", PublishBaseURL: "http://node.example:18090/", + TLSServerName: "node.example", CertificateFingerprint: strings.Repeat("ab:", 31) + "ab", + Enabled: true, Labels: map[string]string{"zone": "test"}, + } +} + +type nodeTestSessionFactory struct{ db *gorm.DB } + +func (f nodeTestSessionFactory) NewSession() entity.Session { return f.db.Begin() } +func (f nodeTestSessionFactory) DB() entity.Session { return f.db } + +func newNodeTestService(t *testing.T, repository repositorystoragenoderepo.Repository) ( + Service, sqlmock.Sqlmock, *sql.DB, +) { + t.Helper() + sqlDB, mock, err := sqlmock.New() + if err != nil { + t.Fatal(err) + } + gormDB, err := gorm.Open(mysql.New(mysql.Config{Conn: sqlDB, SkipInitializeWithVersion: true}), + &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) + if err != nil { + t.Fatal(err) + } + return newService(nodeTestSessionFactory{db: gormDB}, repository, fakeNodeProjectReader{}), mock, sqlDB +} + +type fakeNodeProjectReader struct{} + +func (fakeNodeProjectReader) DescribeProject(_ entity.Context, req request.DescribeProjectRequest, _ entity.Session) ( + request.DescribeProjectResponse, error, +) { + return request.DescribeProjectResponse{Project: po.Project{Base: po.Base{ID: req.ID}, Creator: "admin"}}, nil +} + +type fakeNodeRepository struct { + node *po.RepositoryStorageNode + assigned int64 + preflight *po.RepositoryPreflightRecord + offlineCutoff time.Time + offlineCount int64 + taskLog *po.RepositoryTaskLogIndex + usages []po.RepositoryNodeRepositoryUsage +} + +func (f *fakeNodeRepository) ReplaceRepositoryUsages(_ string, values []po.RepositoryNodeRepositoryUsage, + _ entity.Session, +) error { + f.usages = append([]po.RepositoryNodeRepositoryUsage(nil), values...) + return nil +} + +func (f *fakeNodeRepository) ListNodeRepositorySummaries(string, entity.Session) ( + []po.RepositoryNodeRepositorySummary, error, +) { + return nil, nil +} + +func (f *fakeNodeRepository) UpsertTaskLogIndex(value *po.RepositoryTaskLogIndex, _ entity.Session) error { + f.taskLog = value + return nil +} +func (f *fakeNodeRepository) GetTaskLogIndex(string, entity.Session) (*po.RepositoryTaskLogIndex, error) { + if f.taskLog == nil { + return nil, repositorystoragenoderepo.ErrTaskLogNotFound + } + return f.taskLog, nil +} + +func (f *fakeNodeRepository) SyncTables() error { return nil } +func (f *fakeNodeRepository) CreateNode(value *po.RepositoryStorageNode, _ entity.Session) error { + f.node = value + return nil +} +func (f *fakeNodeRepository) UpdateNode(value *po.RepositoryStorageNode, _ entity.Session) error { + f.node = value + return nil +} +func (f *fakeNodeRepository) GetNodeByID(string, entity.Session) (*po.RepositoryStorageNode, error) { + if f.node == nil { + return nil, repositorystoragenoderepo.ErrStorageNodeNotFound + } + return f.node, nil +} +func (f *fakeNodeRepository) ListNodes(request.ListRepositoryStorageNodesRequest, entity.Session) ( + []po.RepositoryStorageNode, request.PageResponse, error, +) { + if f.node == nil { + return nil, request.PageResponse{}, nil + } + return []po.RepositoryStorageNode{*f.node}, request.PageResponse{Count: 1}, nil +} +func (f *fakeNodeRepository) DeleteNode(string, entity.Session) error { f.node = nil; return nil } +func (f *fakeNodeRepository) CountAssignedRepositories(string, entity.Session) (int64, error) { + return f.assigned, nil +} +func (f *fakeNodeRepository) MarkOfflineBefore(cutoff time.Time, _ entity.Session) (int64, error) { + f.offlineCutoff = cutoff + return f.offlineCount, nil +} +func (f *fakeNodeRepository) CreatePreflight(value *po.RepositoryPreflightRecord, _ entity.Session) error { + f.preflight = value + return nil +} +func (f *fakeNodeRepository) GetPreflightByID(string, entity.Session) (*po.RepositoryPreflightRecord, error) { + if f.preflight == nil { + return nil, repositorystoragenoderepo.ErrPreflightNotFound + } + return f.preflight, nil +} +func (f *fakeNodeRepository) ListPreflights(request.ListRepositoryPreflightRecordsRequest, entity.Session) ( + []po.RepositoryPreflightRecord, request.PageResponse, error, +) { + if f.preflight == nil { + return nil, request.PageResponse{}, nil + } + return []po.RepositoryPreflightRecord{*f.preflight}, request.PageResponse{Count: 1}, nil +} + +func globalNodeContext() entity.Context { + ctx := entity.NewDefaultContext() + ctx.User = &ftpb.UserInfo{LoginName: "admin", Roles: []*ftpb.ModuleRole{{RoleId: request.DefaultGlobalRoleName}}} + return ctx +} diff --git a/manager/backend/services/footstone/domain/repositoryPackageInstallService/preview_artifact_linux_test.go b/manager/backend/services/footstone/domain/repositoryPackageInstallService/preview_artifact_linux_test.go new file mode 100644 index 0000000000000000000000000000000000000000..a5ae032049647897222e6ceaf4355f789c4fa977 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryPackageInstallService/preview_artifact_linux_test.go @@ -0,0 +1,145 @@ +package repositoryPackageInstallService + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" +) + +// The DNF stub checks the executable script contract, not real cryptography. +// Real OC9 tests separately verify metadata signatures and host trust isolation. +func TestPackagePreviewArtifactExecutesIsolatedTrustPreparation(t *testing.T) { + for _, test := range []struct { + name string + scenario string + wantCalls int + wantOK bool + }{ + {name: "cold target", scenario: "cold", wantCalls: 2, wantOK: true}, + {name: "warm target still uses private cache", scenario: "warm", wantCalls: 2, wantOK: true}, + {name: "bad metadata signature stops preview", scenario: "bad-signature", wantCalls: 1}, + {name: "missing metadata key stops preview", scenario: "missing-key", wantCalls: 1}, + {name: "dependency failure stays failed", scenario: "dependency-failure", wantCalls: 2}, + } { + t.Run(test.name, func(t *testing.T) { + root := t.TempDir() + bin := filepath.Join(root, "bin") + if err := os.Mkdir(bin, 0700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(bin, "dnf"), []byte(previewDNFStub), 0700); err != nil { + t.Fatal(err) + } + config := &request.GeneratedRepositoryConfig{ + RepoID: "ocmanager-signed", ActiveRevisionID: "22222222-2222-4222-8222-222222222222", + ImmutableRevision: true, + Content: "[ocmanager-signed]\nbaseurl=https://repo.example/revisions/22222222-2222-4222-8222-222222222222\n" + + "enabled=1\ngpgcheck=1\nrepo_gpgcheck=1\ngpgkey=https://repo.example/keys/authorized.key\n", + } + artifact, err := buildPackageInstallArtifact(config, []string{"demo-package"}, true) + if err != nil { + t.Fatal(err) + } + runFile := filepath.Join(root, "run.sh") + if err = os.WriteFile(runFile, []byte(artifactScript(t, artifact)), 0700); err != nil { + t.Fatal(err) + } + command := exec.Command("bash", runFile) + command.Env = append(os.Environ(), "PATH="+bin+":"+os.Getenv("PATH"), + "OCM_FAKE_ROOT="+root, "OCM_FAKE_SCENARIO="+test.scenario) + output, runErr := command.CombinedOutput() + if (runErr == nil) != test.wantOK { + t.Fatalf("script error = %v, want success %v; output:\n%s", runErr, test.wantOK, output) + } + if strings.Contains(string(output), "OCM_PACKAGE_PREVIEW_OK") != test.wantOK { + t.Fatalf("unexpected preview success marker:\n%s", output) + } + callBytes, err := os.ReadFile(filepath.Join(root, "calls")) + if err != nil { + t.Fatal(err) + } + calls := strings.Split(strings.TrimSpace(string(callBytes)), "\n") + if len(calls) != test.wantCalls { + t.Fatalf("DNF calls = %q, want %d; output:\n%s", calls, test.wantCalls, output) + } + first := strings.Split(calls[0], "|") + if len(first) != 9 || first[0] != "makecache" || first[6] != "yes" || first[7] != "" { + t.Fatalf("metadata preparation call = %q", calls[0]) + } + workDir := first[3] + if !strings.HasSuffix(first[1], "/cache") || first[1] != filepath.Join(workDir, "cache") || + first[2] != filepath.Join(workDir, "persist") || first[4] != filepath.Join(workDir, "logs") || + first[5] != config.RepoID || first[8] != "required" { + t.Fatalf("metadata state was not isolated to one repository: %q", calls[0]) + } + if test.wantCalls == 2 { + second := strings.Split(calls[1], "|") + if len(second) != 9 || second[0] != "install" || second[6] != "" || second[7] != "no" || + strings.Join(first[1:6], "|") != strings.Join(second[1:6], "|") || second[8] != "required" { + t.Fatalf("preview did not reuse isolated trust without installing: %q", calls) + } + } + if _, err = os.Stat(workDir); !os.IsNotExist(err) { + t.Fatalf("temporary DNF state survived script exit: %s, %v", workDir, err) + } + configCopy, err := os.ReadFile(filepath.Join(root, "repository.repo")) + if err != nil || string(configCopy) != config.Content { + t.Fatalf("generated GPG/revision policy changed: %q, %v", configCopy, err) + } + }) + } +} + +const previewDNFStub = `#!/usr/bin/env bash +set -euo pipefail +test "${LC_ALL}" = C +test "${LANG}" = C +action= cache= persist= repos= logs= repo= yes= no= skip= +disabled= +for argument in "$@"; do + case "${argument}" in + --setopt=reposdir=*) repos=${argument#--setopt=reposdir=} ;; + --setopt=cachedir=*) cache=${argument#--setopt=cachedir=} ;; + --setopt=persistdir=*) persist=${argument#--setopt=persistdir=} ;; + --setopt=logdir=*) logs=${argument#--setopt=logdir=} ;; + --setopt=ocmanager-signed.skip_if_unavailable=False) skip=required ;; + --disablerepo=\*) disabled=yes ;; + --enablerepo=*) repo=${argument#--enablerepo=} ;; + -y) yes=yes ;; + --assumeno) no=no ;; + makecache|install) action=${argument} ;; + demo-package) ;; + *) printf 'unexpected DNF option: %s\n' "${argument}" >&2; exit 90 ;; + esac +done +printf '%s\n' "${action}|${cache}|${persist}|${repos}|${logs}|${repo}|${yes}|${no}|${skip}" >>"${OCM_FAKE_ROOT}/calls" +test "${disabled}" = yes +test "${repo}" = ocmanager-signed +cp "${repos}/repository.repo" "${OCM_FAKE_ROOT}/repository.repo" +if [[ ${action} = makecache ]]; then + test "${yes}" = yes + test -z "${no}" + case "${OCM_FAKE_SCENARIO}" in + bad-signature) printf 'Error: repomd.xml GPG signature verification error: Bad GPG signature\n'; exit 32 ;; + missing-key) printf 'Error: configured metadata public key is unavailable\n'; exit 34 ;; + esac + mkdir -p "${cache}/pubring" "${persist}" "${logs}" + printf 'configured-metadata-key\n' >"${cache}/pubring/trusted-key" + printf 'Metadata cache created.\n' + exit 0 +fi +test "${action}" = install +test "${no}" = no +test -z "${yes}" +test -f "${cache}/pubring/trusted-key" +if [[ ${OCM_FAKE_SCENARIO} = dependency-failure ]]; then + printf 'Error: nothing provides required-library\n' + exit 1 +fi +printf 'Dependencies resolved.\nTransaction Summary\nInstall 1 Package\nOperation aborted.\n' +exit 1 +` diff --git a/manager/backend/services/footstone/domain/repositoryPackageInstallService/service.go b/manager/backend/services/footstone/domain/repositoryPackageInstallService/service.go new file mode 100644 index 0000000000000000000000000000000000000000..5ebd386ee989e9cab628858b41c959ea51cb4dd7 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryPackageInstallService/service.go @@ -0,0 +1,870 @@ +package repositoryPackageInstallService + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "net" + "regexp" + "sort" + "strconv" + "strings" + "time" + "unicode" + + "github.com/google/uuid" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorycontentrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorypackageinstallrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorytaskrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryTaskService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/tmsclient" +) + +const ( + previewTTL = 10 * time.Minute + defaultTimeout = 600 + minimumTimeout = 60 + maximumTimeout = 1800 + maximumPackageCount = 100 + maximumRawOutput = 16 << 10 +) + +var ( + ErrPreviewNotReady = errors.New("PREVIEW_NOT_READY: package installation preview is not ready") + ErrPreviewExpired = errors.New("PREVIEW_EXPIRED: package installation preview has expired") + ErrPreviewChanged = errors.New("REVISION_CHANGED: package installation preview digest or revision changed") + ErrPreviewForbidden = errors.New("PREVIEW_FORBIDDEN: package installation preview belongs to another user") + ErrInstallForbidden = errors.New("PACKAGE_INSTALL_FORBIDDEN: package installation belongs to another project") + ErrTargetUnavailable = errors.New("PACKAGE_INSTALL_TARGET_UNAVAILABLE: package installation target is unavailable") +) + +type repositoryManager interface { + Describe(entity.Context, string) (*po.SoftwareRepository, error) +} + +type configProvider interface { + GenerateRepositoryConfig(entity.Context, string, bool) (*request.GeneratedRepositoryConfig, error) +} + +type PreviewResult struct { + Preview *po.RepositoryOperationPreview + Payload request.RepositoryPackageInstallPreviewPayload +} + +type ConfirmResult struct { + Install *po.RepositoryPackageInstall + Task *po.RepositoryTask +} + +type Service = *service + +type service struct { + db entity.SessionFactory + repositories repositoryManager + configs configProvider + content repositorycontentrepo.Repository + tasks repositoryTaskService.Service + taskRecords repositorytaskrepo.Repository + targets repositorytaskrepo.TargetRepository + records repositorypackageinstallrepo.Repository + tms tmsclient.Client + now func() time.Time +} + +func New(db *database.DBClient, repositories repositoryManager, configs configProvider, + tasks repositoryTaskService.Service, tms tmsclient.Client, +) Service { + return &service{db: db, repositories: repositories, configs: configs, + content: repositorycontentrepo.New(db), tasks: tasks, taskRecords: repositorytaskrepo.New(db), + targets: repositorytaskrepo.NewTargetRepository(db), records: repositorypackageinstallrepo.New(db), + tms: tms, now: func() time.Time { return time.Now().UTC() }} +} + +func (s *service) SyncTables() error { return s.records.SyncTables() } + +func (s *service) Preview(ctx entity.Context, input request.PreviewRepositoryPackageInstallRequest) ( + *PreviewResult, error, +) { + input.RepositoryID = strings.TrimSpace(input.RepositoryID) + repository, err := s.repositories.Describe(ctx, input.RepositoryID) + if err != nil { + return nil, err + } + input.RevisionID = strings.TrimSpace(input.RevisionID) + if input.RevisionID == "" { + input.RevisionID = repository.ActiveRevisionID + } + if uuid.Validate(input.RevisionID) != nil || input.RevisionID != repository.ActiveRevisionID { + return nil, fmt.Errorf("revision_id must be the current immutable active revision") + } + revision, err := s.content.GetRevision(repository.ID, input.RevisionID, nil) + if err != nil { + return nil, err + } + if revision.Status != request.RepositoryRevisionStatusPublished || revision.PublishedAt == nil { + return nil, fmt.Errorf("repository revision is not published") + } + packageNames, err := normalizePackageNames(input.PackageNames) + if err != nil { + return nil, err + } + config, err := s.configs.GenerateRepositoryConfig(ctx, repository.ID, true) + if err != nil { + return nil, err + } + if config.ActiveRevisionID != revision.ID || !config.ImmutableRevision { + return nil, fmt.Errorf("generated repository config is not bound to the requested immutable revision") + } + targets, targetIPs, err := s.resolveTargets(ctx, input.TargetAgentIDs) + if err != nil { + return nil, err + } + timeout, err := normalizeTimeout(input.TimeoutSeconds) + if err != nil { + return nil, err + } + artifact, err := buildPackageInstallArtifact(config, packageNames, true) + if err != nil { + return nil, err + } + artifactMD5, err := s.tms.UploadArtifact(ctx, "repository-package-preview-"+uuid.NewString()+".tar.gz", artifact) + if err != nil { + return nil, fmt.Errorf("upload package preview artifact: %w", err) + } + tmsTaskID, err := s.tms.Publish(ctx, artifactMD5, targetIPs, timeout) + if err != nil { + return nil, fmt.Errorf("dispatch package preview: %w", err) + } + detail, err := s.waitTMS(ctx, tmsTaskID, timeout) + if err != nil { + return nil, err + } + targetResults := mapTMSResults(targets, detail, packageNames) + status := request.RepositoryOperationPreviewReady + for _, target := range targetResults { + if target.Status != request.RepositoryTaskStatusSuccess { + status = request.RepositoryOperationPreviewFailed + break + } + } + payload := request.RepositoryPackageInstallPreviewPayload{ + RepositoryID: repository.ID, RevisionID: revision.ID, RepoID: config.RepoID, + RepoConfig: config.Content, PackageNames: packageNames, Targets: targetResults, Timeout: timeout, + } + payloadJSON, err := json.Marshal(payload) + if err != nil { + return nil, err + } + digestBytes := sha256.Sum256(payloadJSON) + now := s.now() + preview := &po.RepositoryOperationPreview{ + ID: uuid.NewString(), ProjectID: repository.ProjectID, + OperationType: request.RepositoryOperationPackageInstall, ResourceID: repository.ID, + ResourceVersion: revision.ID, PreviewDigest: hex.EncodeToString(digestBytes[:]), + PayloadJSON: payloadJSON, Status: status, ExpiresAt: now.Add(previewTTL), CreatedBy: ctx.User.LoginName, + } + if err = s.records.CreatePreview(preview, nil); err != nil { + return nil, err + } + return &PreviewResult{Preview: preview, Payload: payload}, nil +} + +func (s *service) Confirm(ctx entity.Context, input request.ConfirmRepositoryPackageInstallRequest) ( + result *ConfirmResult, err error, +) { + input.PreviewID, input.PreviewDigest = strings.TrimSpace(input.PreviewID), strings.ToLower(strings.TrimSpace(input.PreviewDigest)) + if uuid.Validate(input.PreviewID) != nil || len(input.PreviewDigest) != sha256.Size*2 { + return nil, ErrPreviewChanged + } + var payload request.RepositoryPackageInstallPreviewPayload + var install *po.RepositoryPackageInstall + var task *po.RepositoryTask + alreadyConfirmed := false + err = func() (transactionErr error) { + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&transactionErr) + preview, transactionErr := s.records.GetPreview(input.PreviewID, true, tx) + if transactionErr != nil { + return transactionErr + } + now := s.now() + if preview.CreatedBy != ctx.User.LoginName { + return ErrPreviewForbidden + } + if preview.PreviewDigest != input.PreviewDigest { + return ErrPreviewChanged + } + if preview.Status == request.RepositoryOperationPreviewConfirmed { + install, transactionErr = s.records.GetInstallByPreview(preview.ID, tx) + if transactionErr != nil { + return transactionErr + } + task, transactionErr = s.taskRecords.GetByID(install.RepositoryTaskID, tx) + alreadyConfirmed = transactionErr == nil + return transactionErr + } + if preview.Status != request.RepositoryOperationPreviewReady { + return ErrPreviewNotReady + } + if !now.Before(preview.ExpiresAt) { + return ErrPreviewExpired + } + if transactionErr = json.Unmarshal(preview.PayloadJSON, &payload); transactionErr != nil || + payload.RepositoryID != preview.ResourceID || payload.RevisionID != preview.ResourceVersion { + return ErrPreviewChanged + } + repository, describeErr := s.repositories.Describe(ctx, payload.RepositoryID) + if describeErr != nil || repository.ProjectID != preview.ProjectID { + return ErrPreviewChanged + } + revision, revisionErr := s.content.GetRevision(repository.ID, payload.RevisionID, tx) + if revisionErr != nil || revision.Status != request.RepositoryRevisionStatusPublished || revision.PublishedAt == nil { + return ErrPreviewChanged + } + agentIDs := make([]uint64, 0, len(payload.Targets)) + for _, target := range payload.Targets { + if target.Status != request.RepositoryTaskStatusSuccess || target.AgentID == 0 { + return ErrPreviewNotReady + } + agentIDs = append(agentIDs, target.AgentID) + } + taskPayload, marshalErr := json.Marshal(map[string]interface{}{ + "preview_id": preview.ID, "preview_digest": preview.PreviewDigest, + "repository_id": payload.RepositoryID, "revision_id": payload.RevisionID, + "package_names": payload.PackageNames, + }) + if marshalErr != nil { + return marshalErr + } + task, transactionErr = s.tasks.EnqueuePackageInstallInSession(ctx, request.EnqueueRepositoryTaskRequest{ + RepositoryID: repository.ID, RevisionID: revision.ID, Type: request.RepositoryTaskTypePackageInstall, + PayloadJSON: string(taskPayload), MaxAttempts: 1, + IdempotencyKey: "package-install:" + preview.ID, + }, agentIDs, tx) + if transactionErr != nil { + return transactionErr + } + install = &po.RepositoryPackageInstall{ + ID: uuid.NewString(), ProjectID: repository.ProjectID, RepositoryID: repository.ID, + RevisionID: revision.ID, PreviewID: preview.ID, RepositoryTaskID: task.ID, + Status: request.RepositoryPackageInstallPending, TimeoutSeconds: payload.Timeout, + RequestedBy: ctx.User.LoginName, + } + if transactionErr = s.records.CreateInstall(install, tx); transactionErr != nil { + return transactionErr + } + preview.Status, preview.ConfirmedAt = request.RepositoryOperationPreviewConfirmed, &now + return s.records.UpdatePreview(preview, tx) + }() + if err != nil { + return nil, err + } + if alreadyConfirmed { + if current, describeErr := s.tasks.Describe(ctx, task.ID); describeErr == nil { + task = current + } + return &ConfirmResult{Install: install, Task: task}, nil + } + if err = s.dispatchInstall(ctx, install, task, payload); err != nil { + _ = s.failDispatch(install, task.ID, "PACKAGE_INSTALL_DISPATCH_FAILED", err.Error()) + return nil, err + } + if current, describeErr := s.tasks.Describe(ctx, task.ID); describeErr == nil { + task = current + } + return &ConfirmResult{Install: install, Task: task}, nil +} + +func (s *service) Describe(ctx entity.Context, id string) (*po.RepositoryPackageInstall, error) { + if uuid.Validate(strings.TrimSpace(id)) != nil { + return nil, fmt.Errorf("install_id must be a UUID") + } + value, err := s.records.GetInstall(id, nil) + if err != nil { + return nil, err + } + repository, err := s.repositories.Describe(ctx, value.RepositoryID) + if err != nil { + return nil, err + } + if repository.ProjectID != value.ProjectID { + return nil, ErrInstallForbidden + } + return value, nil +} + +func (s *service) Reconcile(ctx context.Context) error { + values, err := s.records.ListActiveInstalls(nil) + if err != nil { + return err + } + var firstError error + for index := range values { + if err = s.reconcileOne(ctx, &values[index]); err != nil && firstError == nil { + firstError = err + } + } + return firstError +} + +func (s *service) dispatchInstall(ctx context.Context, install *po.RepositoryPackageInstall, + task *po.RepositoryTask, payload request.RepositoryPackageInstallPreviewPayload, +) error { + config := &request.GeneratedRepositoryConfig{RepoID: payload.RepoID, Content: payload.RepoConfig, + ActiveRevisionID: payload.RevisionID, ImmutableRevision: true} + artifact, err := buildPackageInstallArtifact(config, payload.PackageNames, false) + if err != nil { + return err + } + artifactMD5, err := s.tms.UploadArtifact(ctx, "repository-package-install-"+install.ID+".tar.gz", artifact) + if err != nil { + return fmt.Errorf("upload package install artifact: %w", err) + } + addresses := make([]string, 0, len(payload.Targets)) + for _, target := range payload.Targets { + addresses = append(addresses, target.IP) + } + tmsTaskID, err := s.tms.Publish(ctx, artifactMD5, addresses, payload.Timeout) + if err != nil { + return fmt.Errorf("dispatch package install: %w", err) + } + now := s.now() + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + install.TMSTaskID, install.ArtifactMD5, install.Status = tmsTaskID, artifactMD5, request.RepositoryPackageInstallRunning + if err = s.records.UpdateInstall(install, tx); err != nil { + return err + } + task, err = s.taskRecords.GetByIDForUpdate(task.ID, tx) + if err != nil { + return err + } + task.Status, task.CurrentStep, task.ProgressPercent = request.RepositoryTaskStatusRunning, "installing packages through TMS", 10 + task.StartedAt = &now + if err = s.taskRecords.Update(task, tx); err != nil { + return err + } + targets, err := s.targets.ListTargets(task.ID, tx) + if err != nil { + return err + } + for index := range targets { + targets[index].Status, targets[index].ProgressPercent, targets[index].StartedAt = + request.RepositoryTaskStatusRunning, 10, &now + resultJSON, _ := json.Marshal(map[string]string{"tms_task_id": tmsTaskID}) + targets[index].ResultJSON = resultJSON + if err = s.targets.UpdateTarget(&targets[index], tx); err != nil { + return err + } + } + return nil +} + +func (s *service) reconcileOne(ctx context.Context, install *po.RepositoryPackageInstall) (err error) { + if install.TMSTaskID == "" { + return nil + } + task, err := s.taskRecords.GetByID(install.RepositoryTaskID, nil) + if err != nil { + return err + } + if task.Status == request.RepositoryTaskStatusCancelRequested { + _ = s.tms.Cancel(ctx, install.TMSTaskID, "repository package installation canceled") + } + if s.now().After(install.CreatedAt.Add(time.Duration(install.TimeoutSeconds+60) * time.Second)) { + _ = s.tms.Cancel(ctx, install.TMSTaskID, "repository package installation timed out") + return s.failDispatch(install, task.ID, "PACKAGE_INSTALL_TIMEOUT", "package installation did not reach a terminal state") + } + detail, err := s.tms.Detail(ctx, install.TMSTaskID) + if err != nil || !isTMSTerminal(detail.Status) { + return err + } + return s.completeInstall(install, task, detail) +} + +func (s *service) completeInstall(install *po.RepositoryPackageInstall, task *po.RepositoryTask, + detail tmsclient.TaskDetail, +) (err error) { + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + task, err = s.taskRecords.GetByIDForUpdate(task.ID, tx) + if err != nil { + return err + } + targets, err := s.targets.ListTargets(task.ID, tx) + if err != nil { + return err + } + byAgent := make(map[uint64]tmsclient.TargetResult, len(detail.Targets)) + for _, target := range detail.Targets { + byAgent[target.AgentID] = target + } + now := s.now() + succeeded, failed, canceled := 0, 0, 0 + for index := range targets { + value, ok := byAgent[targets[index].TMSAgentID] + if !ok { + value = tmsclient.TargetResult{AgentID: targets[index].TMSAgentID, Status: "FAILED", + ErrorCode: "TMS_TARGET_RESULT_MISSING", ErrorMessage: "TMS did not return this target"} + } + targets[index].Status = normalizeTargetStatus(value.Status) + targets[index].ProgressPercent, targets[index].FinishedAt = 100, &now + targets[index].ErrorCode, targets[index].ErrorMessage = value.ErrorCode, bounded(value.ErrorMessage, 2048) + result, _ := json.Marshal(map[string]interface{}{ + "tms_task_id": install.TMSTaskID, "exit_code": value.ExitCode, + "raw_output": bounded(value.Output, maximumRawOutput), "output_ref": value.OutputRef, + "output_truncated": value.OutputTruncated || len(value.Output) > maximumRawOutput, + "repository_id": install.RepositoryID, "revision_id": install.RevisionID, + }) + targets[index].ResultJSON = result + switch targets[index].Status { + case request.RepositoryTaskStatusSuccess: + succeeded++ + case request.RepositoryTaskStatusCanceled: + canceled++ + default: + failed++ + } + if err = s.targets.UpdateTarget(&targets[index], tx); err != nil { + return err + } + } + task.Status, install.Status = aggregateStatus(succeeded, failed, canceled, len(targets)) + task.ProgressPercent, task.FinishedAt, task.CurrentStep = 100, &now, "package installation finished" + if task.Status == request.RepositoryTaskStatusFailed || task.Status == request.RepositoryTaskStatusPartialSuccess { + task.ErrorCode, task.ErrorMessage = "PACKAGE_INSTALL_TARGET_FAILED", fmt.Sprintf("%d of %d targets failed", failed, len(targets)) + } + if err = s.taskRecords.Update(task, tx); err != nil { + return err + } + return s.records.UpdateInstall(install, tx) +} + +func (s *service) failDispatch(install *po.RepositoryPackageInstall, taskID, code, message string) (err error) { + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + task, err := s.taskRecords.GetByIDForUpdate(taskID, tx) + if err != nil { + return err + } + now := s.now() + task.Status, task.ProgressPercent, task.ErrorCode, task.ErrorMessage = + request.RepositoryTaskStatusFailed, 100, code, bounded(message, 2048) + task.FinishedAt, task.CurrentStep = &now, "package installation failed" + if err = s.taskRecords.Update(task, tx); err != nil { + return err + } + targets, err := s.targets.ListTargets(taskID, tx) + if err != nil { + return err + } + for index := range targets { + targets[index].Status, targets[index].ProgressPercent = request.RepositoryTaskStatusFailed, 100 + targets[index].ErrorCode, targets[index].ErrorMessage, targets[index].FinishedAt = code, bounded(message, 2048), &now + if err = s.targets.UpdateTarget(&targets[index], tx); err != nil { + return err + } + } + install.Status = request.RepositoryPackageInstallFailed + return s.records.UpdateInstall(install, tx) +} + +func (s *service) resolveTargets(ctx context.Context, ids []uint64) ( + []request.RepositoryPackageInstallTarget, []string, error, +) { + if len(ids) == 0 || len(ids) > 100 { + return nil, nil, fmt.Errorf("target_agent_ids must contain between 1 and 100 entries") + } + nodes, err := s.tms.ListNodes(ctx) + if err != nil { + return nil, nil, err + } + byID := make(map[uint64]request.RepositoryDistributionNode, len(nodes)) + for _, node := range nodes { + byID[node.AgentID] = node + } + seen := make(map[uint64]struct{}, len(ids)) + targets := make([]request.RepositoryPackageInstallTarget, 0, len(ids)) + for _, id := range ids { + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + node, ok := byID[id] + if !ok || !node.Connected || net.ParseIP(node.IP) == nil { + return nil, nil, fmt.Errorf("%w: agent %d", ErrTargetUnavailable, id) + } + targets = append(targets, request.RepositoryPackageInstallTarget{AgentID: id, IP: node.IP}) + } + sort.Slice(targets, func(i, j int) bool { return targets[i].AgentID < targets[j].AgentID }) + ips := make([]string, 0, len(targets)) + for _, target := range targets { + ips = append(ips, target.IP) + } + return targets, ips, nil +} + +func (s *service) waitTMS(ctx context.Context, taskID string, timeoutSeconds int) (tmsclient.TaskDetail, error) { + deadline := time.NewTimer(time.Duration(timeoutSeconds+30) * time.Second) + defer deadline.Stop() + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + _ = s.tms.Cancel(context.Background(), taskID, "package installation preview canceled") + return tmsclient.TaskDetail{}, ctx.Err() + case <-deadline.C: + _ = s.tms.Cancel(context.Background(), taskID, "package installation preview timed out") + return tmsclient.TaskDetail{}, fmt.Errorf("package installation preview timed out") + case <-ticker.C: + detail, err := s.tms.Detail(ctx, taskID) + if err == nil && isTMSTerminal(detail.Status) { + return detail, nil + } + } + } +} + +func buildPackageInstallArtifact(config *request.GeneratedRepositoryConfig, packageNames []string, + preview bool, +) ([]byte, error) { + if config == nil || !config.ImmutableRevision || config.RepoID == "" || config.Content == "" { + return nil, fmt.Errorf("immutable repository config is required") + } + quotedPackages := make([]string, 0, len(packageNames)) + for _, packageName := range packageNames { + quotedPackages = append(quotedPackages, shellQuote(packageName)) + } + script := "#!/usr/bin/env bash\nset -euo pipefail\nexport LC_ALL=C\nexport LANG=C\n" + + "work_dir=$(mktemp -d /tmp/ocmanager-package-install.XXXXXX)\n" + + "trap 'rm -rf \"${work_dir}\"' EXIT\n" + + "cat >\"${work_dir}/repository.repo\" <<'OCM_REPO_CONFIG'\n" + config.Content + + "OCM_REPO_CONFIG\n" + dnfOptions := "--setopt=\"reposdir=${work_dir}\" --disablerepo='*' --enablerepo=" + shellQuote(config.RepoID) + " " + if preview { + // DNF stores metadata signing keys in the repository cache, separate from rpmdb. + // Prepare only this temporary trust/cache before assumeno rejects all prompts. + dnfOptions += "--setopt=\"cachedir=${work_dir}/cache\" " + + "--setopt=\"persistdir=${work_dir}/persist\" --setopt=\"logdir=${work_dir}/logs\" " + + "--setopt=" + shellQuote(config.RepoID+".skip_if_unavailable=False") + " " + script += "set +e\ndnf " + dnfOptions + "-y makecache >\"${work_dir}/metadata.out\" 2>&1\n" + + "metadata_exit=$?\nset -e\ncat \"${work_dir}/metadata.out\"\n" + + "if [ \"${metadata_exit}\" -ne 0 ]; then exit \"${metadata_exit}\"; fi\n" + } + script += "set +e\ndnf " + if !preview { + script += "-q " + } + script += dnfOptions + if preview { + script += "--assumeno install " + strings.Join(quotedPackages, " ") + " >\"${work_dir}/dnf.out\" 2>&1\n" + } else { + script += "-y install " + strings.Join(quotedPackages, " ") + " >\"${work_dir}/dnf.out\" 2>&1\n" + } + script += "dnf_exit=$?\nset -e\ncat \"${work_dir}/dnf.out\"\n" + if preview { + script += "if grep -Eq 'Transaction Summary|Nothing to do|is already installed' \"${work_dir}/dnf.out\" && " + + "! grep -Eq '^(Error:|No match for argument:)' \"${work_dir}/dnf.out\"; then\n" + + " printf 'OCM_PACKAGE_PREVIEW_OK revision=%s repo=%s\\n' " + shellQuote(config.ActiveRevisionID) + " " + shellQuote(config.RepoID) + "\n" + + " exit 0\nfi\n" + } else { + script += "if [ \"${dnf_exit}\" -eq 0 ]; then printf 'OCM_PACKAGE_INSTALL_OK revision=%s repo=%s\\n' " + + shellQuote(config.ActiveRevisionID) + " " + shellQuote(config.RepoID) + "; fi\n" + } + script += "exit \"${dnf_exit}\"\n" + + var buffer bytes.Buffer + gzipWriter := gzip.NewWriter(&buffer) + tarWriter := tar.NewWriter(gzipWriter) + if err := tarWriter.WriteHeader(&tar.Header{Name: "run.sh", Mode: 0o700, Size: int64(len(script))}); err != nil { + return nil, err + } + if _, err := tarWriter.Write([]byte(script)); err != nil { + return nil, err + } + if err := tarWriter.Close(); err != nil { + return nil, err + } + if err := gzipWriter.Close(); err != nil { + return nil, err + } + return buffer.Bytes(), nil +} + +func normalizePackageNames(values []string) ([]string, error) { + if len(values) == 0 || len(values) > maximumPackageCount { + return nil, fmt.Errorf("package_names must contain between 1 and %d entries", maximumPackageCount) + } + seen := make(map[string]struct{}, len(values)) + result := make([]string, 0, len(values)) + for _, value := range values { + value = strings.TrimSpace(value) + if value == "" || len(value) > 512 || strings.HasPrefix(value, "-") || strings.IndexFunc(value, func(r rune) bool { + return unicode.IsSpace(r) || unicode.IsControl(r) + }) >= 0 { + return nil, fmt.Errorf("invalid package name %q", value) + } + if _, ok := seen[value]; ok { + continue + } + seen[value] = struct{}{} + result = append(result, value) + } + sort.Strings(result) + return result, nil +} + +func normalizeTimeout(value int) (int, error) { + if value == 0 { + return defaultTimeout, nil + } + if value < minimumTimeout || value > maximumTimeout { + return 0, fmt.Errorf("timeout_seconds must be between %d and %d", minimumTimeout, maximumTimeout) + } + return value, nil +} + +func mapTMSResults(targets []request.RepositoryPackageInstallTarget, + detail tmsclient.TaskDetail, requestedPackages []string, +) []request.RepositoryPackageInstallTarget { + byAgent := make(map[uint64]tmsclient.TargetResult, len(detail.Targets)) + for _, value := range detail.Targets { + byAgent[value.AgentID] = value + } + result := make([]request.RepositoryPackageInstallTarget, 0, len(targets)) + for _, target := range targets { + value, ok := byAgent[target.AgentID] + if !ok { + value = tmsclient.TargetResult{Status: "FAILED", ErrorCode: "TMS_TARGET_RESULT_MISSING", + ErrorMessage: "TMS did not return this target"} + } + target.Status, target.ExitCode = normalizeTargetStatus(value.Status), value.ExitCode + target.ErrorCode, target.ErrorMessage = value.ErrorCode, bounded(value.ErrorMessage, 2048) + target.RawOutput, target.OutputTruncated = bounded(value.Output, maximumRawOutput), + value.OutputTruncated || len(value.Output) > maximumRawOutput + preview := parseDNFPreviewOutput(value.Output, requestedPackages) + target.SelectedPackages = preview.SelectedPackages + target.DependencyPackages = preview.DependencyPackages + target.UpgradePackages = preview.UpgradePackages + target.ReplacementPackages = preview.ReplacementPackages + target.DownloadBytes = preview.DownloadBytes + target.DiskChangeBytes = preview.DiskChangeBytes + if value.StartedAt > 0 { + target.StartedAt = time.Unix(value.StartedAt, 0).UTC() + } + if value.EndedAt > 0 { + target.FinishedAt = time.Unix(value.EndedAt, 0).UTC() + } + result = append(result, target) + } + return result +} + +type dnfPreviewResult struct { + SelectedPackages []string + DependencyPackages []string + UpgradePackages []string + ReplacementPackages []string + DownloadBytes int64 + DiskChangeBytes int64 +} + +type dnfPackageSection int + +const ( + dnfPackageSectionNone dnfPackageSection = iota + dnfPackageSectionSelected + dnfPackageSectionDependency + dnfPackageSectionUpgrade + dnfPackageSectionReplacement +) + +var dnfSizeLinePattern = regexp.MustCompile(`(?i)^([0-9]+(?:\.[0-9]+)?)\s*([kmgt]?i?b?|[kmgt])$`) + +// parseDNFPreviewOutput parses the stable English output produced by the +// LC_ALL=C/LANG=C preview script. Raw output remains available for diagnosis; +// these fields provide a structured transaction summary to API consumers. +func parseDNFPreviewOutput(output string, requestedPackages []string) dnfPreviewResult { + result := dnfPreviewResult{} + section := dnfPackageSectionNone + for _, rawLine := range strings.Split(output, "\n") { + line := strings.TrimSpace(rawLine) + switch line { + case "Installing:", "Reinstalling:": + section = dnfPackageSectionSelected + continue + case "Installing dependencies:", "Installing weak dependencies:": + section = dnfPackageSectionDependency + continue + case "Upgrading:", "Downgrading:": + section = dnfPackageSectionUpgrade + continue + case "Replacing:": + section = dnfPackageSectionReplacement + continue + case "Transaction Summary": + section = dnfPackageSectionNone + continue + } + if value, ok := parseDNFSizeLine(line, "Total download size:", "Download size:", "Total size:"); ok { + result.DownloadBytes = value + continue + } + if value, ok := parseDNFSizeLine(line, "Installed size:"); ok { + result.DiskChangeBytes = value + continue + } + if value, ok := parseDNFSizeLine(line, "Freed space:"); ok { + result.DiskChangeBytes = -value + continue + } + packageNEVRA, ok := parseDNFPackageRow(line) + if !ok { + continue + } + switch section { + case dnfPackageSectionSelected: + result.SelectedPackages = append(result.SelectedPackages, packageNEVRA) + case dnfPackageSectionDependency: + result.DependencyPackages = append(result.DependencyPackages, packageNEVRA) + case dnfPackageSectionUpgrade: + result.UpgradePackages = append(result.UpgradePackages, packageNEVRA) + case dnfPackageSectionReplacement: + result.ReplacementPackages = append(result.ReplacementPackages, packageNEVRA) + } + } + if len(result.SelectedPackages) == 0 { + result.SelectedPackages = append([]string(nil), requestedPackages...) + } + result.SelectedPackages = uniqueSorted(result.SelectedPackages) + result.DependencyPackages = uniqueSorted(result.DependencyPackages) + result.UpgradePackages = uniqueSorted(result.UpgradePackages) + result.ReplacementPackages = uniqueSorted(result.ReplacementPackages) + return result +} + +func parseDNFPackageRow(line string) (string, bool) { + fields := strings.Fields(line) + if len(fields) < 5 || fields[0] == "Package" || !isRPMArchitecture(fields[1]) { + return "", false + } + return fields[0] + "-" + fields[2] + "." + fields[1], true +} + +func isRPMArchitecture(value string) bool { + switch strings.ToLower(value) { + case "x86_64", "aarch64", "noarch", "i686", "i386", "src": + return true + default: + return false + } +} + +func parseDNFSizeLine(line string, prefixes ...string) (int64, bool) { + for _, prefix := range prefixes { + if !strings.HasPrefix(line, prefix) { + continue + } + matches := dnfSizeLinePattern.FindStringSubmatch(strings.TrimSpace(strings.TrimPrefix(line, prefix))) + if len(matches) != 3 { + return 0, false + } + value, err := strconv.ParseFloat(matches[1], 64) + if err != nil { + return 0, false + } + factor := float64(1) + switch strings.ToLower(matches[2]) { + case "k", "kb", "kib": + factor = 1 << 10 + case "m", "mb", "mib": + factor = 1 << 20 + case "g", "gb", "gib": + factor = 1 << 30 + case "t", "tb", "tib": + factor = 1 << 40 + } + return int64(value*factor + 0.5), true + } + return 0, false +} + +func uniqueSorted(values []string) []string { + seen := make(map[string]struct{}, len(values)) + result := make([]string, 0, len(values)) + for _, value := range values { + value = strings.TrimSpace(value) + if value == "" { + continue + } + if _, ok := seen[value]; ok { + continue + } + seen[value] = struct{}{} + result = append(result, value) + } + sort.Strings(result) + return result +} + +func normalizeTargetStatus(value string) string { + switch strings.ToUpper(strings.TrimSpace(value)) { + case "SUCCESS": + return request.RepositoryTaskStatusSuccess + case "CANCELED", "CANCELLED": + return request.RepositoryTaskStatusCanceled + case "PENDING", "DISPATCHING": + return request.RepositoryTaskStatusPending + case "RUNNING": + return request.RepositoryTaskStatusRunning + default: + return request.RepositoryTaskStatusFailed + } +} + +func isTMSTerminal(value string) bool { + switch strings.ToUpper(strings.TrimSpace(value)) { + case "SUCCESS", "FAILED", "PARTIAL_SUCCESS", "CANCELED", "CANCELLED", "TIMEOUT": + return true + default: + return false + } +} + +func aggregateStatus(success, failed, canceled, total int) (string, string) { + if success == total { + return request.RepositoryTaskStatusSuccess, request.RepositoryPackageInstallSuccess + } + if success > 0 { + return request.RepositoryTaskStatusPartialSuccess, request.RepositoryPackageInstallPartialSuccess + } + if canceled == total { + return request.RepositoryTaskStatusCanceled, request.RepositoryPackageInstallCanceled + } + _ = failed + return request.RepositoryTaskStatusFailed, request.RepositoryPackageInstallFailed +} + +func shellQuote(value string) string { + return "'" + strings.ReplaceAll(value, "'", "'\\''") + "'" +} + +func bounded(value string, limit int) string { + value = strings.TrimSpace(value) + if len(value) <= limit { + return value + } + return value[:limit] +} diff --git a/manager/backend/services/footstone/domain/repositoryPackageInstallService/service_test.go b/manager/backend/services/footstone/domain/repositoryPackageInstallService/service_test.go new file mode 100644 index 0000000000000000000000000000000000000000..984d958a6c1f0a812d193ef4909b62e5dc25252d --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryPackageInstallService/service_test.go @@ -0,0 +1,185 @@ +package repositoryPackageInstallService + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "io" + "strings" + "testing" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/tmsclient" +) + +func TestBuildPackageInstallArtifactPinsImmutableRepositoryAndLocale(t *testing.T) { + config := &request.GeneratedRepositoryConfig{ + RepoID: "ocmanager-private", ActiveRevisionID: "22222222-2222-4222-8222-222222222222", + ImmutableRevision: true, + Content: "[ocmanager-private]\nbaseurl=https://repo.example/repositories/private/revisions/22222222-2222-4222-8222-222222222222\nenabled=1\ngpgcheck=0\n", + } + artifact, err := buildPackageInstallArtifact(config, []string{"demo-package"}, true) + if err != nil { + t.Fatal(err) + } + script := artifactScript(t, artifact) + for _, expected := range []string{ + "export LC_ALL=C", "export LANG=C", "--disablerepo='*'", "--enablerepo='ocmanager-private'", + "dnf --setopt=", "--assumeno install 'demo-package'", "Transaction Summary", + config.ActiveRevisionID, "OCM_PACKAGE_PREVIEW_OK", + } { + if !strings.Contains(script, expected) { + t.Fatalf("preview script does not contain %q:\n%s", expected, script) + } + } + if strings.Contains(script, "/current") { + t.Fatalf("preview script used mutable current URL:\n%s", script) + } +} + +func TestPackageInstallValidationAndAggregate(t *testing.T) { + values, err := normalizePackageNames([]string{"zlib", "bash", "zlib"}) + if err != nil || strings.Join(values, ",") != "bash,zlib" { + t.Fatalf("normalizePackageNames() = %#v, %v", values, err) + } + for _, invalid := range [][]string{{}, {"--allowerasing"}, {"bad package"}} { + if _, err = normalizePackageNames(invalid); err == nil { + t.Fatalf("normalizePackageNames(%#v) accepted invalid input", invalid) + } + } + if task, install := aggregateStatus(2, 0, 0, 2); task != request.RepositoryTaskStatusSuccess || + install != request.RepositoryPackageInstallSuccess { + t.Fatalf("success aggregate = %s/%s", task, install) + } + if task, install := aggregateStatus(1, 1, 0, 2); task != request.RepositoryTaskStatusPartialSuccess || + install != request.RepositoryPackageInstallPartialSuccess { + t.Fatalf("partial aggregate = %s/%s", task, install) + } +} + +func TestPackageInstallPreviewPreservesGPGAndUsesTemporaryTrust(t *testing.T) { + config := &request.GeneratedRepositoryConfig{ + RepoID: "ocmanager-signed", ActiveRevisionID: "22222222-2222-4222-8222-222222222222", + ImmutableRevision: true, + Content: "[ocmanager-signed]\nbaseurl=https://repo.example/revisions/22222222-2222-4222-8222-222222222222\n" + + "enabled=1\ngpgcheck=1\nrepo_gpgcheck=1\ngpgkey=https://repo.example/keys/authorized.key\n", + } + artifact, err := buildPackageInstallArtifact(config, []string{"demo-package"}, true) + if err != nil { + t.Fatal(err) + } + script := artifactScript(t, artifact) + for _, expected := range []string{ + config.Content, "-y makecache", "--assumeno install 'demo-package'", + "--setopt=\"cachedir=${work_dir}/cache\"", "--setopt=\"persistdir=${work_dir}/persist\"", + "--setopt=\"logdir=${work_dir}/logs\"", "--setopt='ocmanager-signed.skip_if_unavailable=False'", + "if [ \"${metadata_exit}\" -ne 0 ]; then exit \"${metadata_exit}\"; fi", + } { + if !strings.Contains(script, expected) { + t.Fatalf("signed preview script does not contain %q:\n%s", expected, script) + } + } + for _, forbidden := range []string{"--nogpgcheck", "gpgcheck=0", "rpm --import", "--installroot", "-y install"} { + if strings.Contains(script, forbidden) { + t.Fatalf("preview contains unsafe or misleading operation %q:\n%s", forbidden, script) + } + } + if strings.Index(script, "-y makecache") > strings.Index(script, "--assumeno install") { + t.Fatal("metadata trust must be prepared before transaction preview") + } +} + +func TestPackageInstallExecutionDoesNotUsePreviewTrust(t *testing.T) { + config := &request.GeneratedRepositoryConfig{ + RepoID: "ocmanager-private", ActiveRevisionID: "22222222-2222-4222-8222-222222222222", + ImmutableRevision: true, + Content: "[ocmanager-private]\nbaseurl=https://repo.example/revisions/22222222-2222-4222-8222-222222222222\ngpgcheck=1\n", + } + artifact, err := buildPackageInstallArtifact(config, []string{"demo-package"}, false) + if err != nil { + t.Fatal(err) + } + script := artifactScript(t, artifact) + if !strings.Contains(script, "-y install 'demo-package'") || + strings.Contains(script, "makecache") || strings.Contains(script, "cachedir=${work_dir}") { + t.Fatalf("execution must retain the confirmed install path:\n%s", script) + } +} + +func TestMapTMSResultsKeepsIndependentTargetResults(t *testing.T) { + targets := []request.RepositoryPackageInstallTarget{{AgentID: 11, IP: "192.0.2.11"}, {AgentID: 12, IP: "192.0.2.12"}} + result := mapTMSResults(targets, tmsclient.TaskDetail{Status: "PARTIAL_SUCCESS", Targets: []tmsclient.TargetResult{ + {AgentID: 11, Status: "SUCCESS", ExitCode: 0, Output: "transaction ready\nTotal download size: 4 k\nInstalled size: 8 k"}, + {AgentID: 12, Status: "FAILED", ExitCode: 1, ErrorCode: "DNF_DEPENDENCY_MISSING", ErrorMessage: "dependency missing"}, + }}, []string{"demo-package"}) + if len(result) != 2 || result[0].Status != request.RepositoryTaskStatusSuccess || + result[1].Status != request.RepositoryTaskStatusFailed || result[1].ErrorCode != "DNF_DEPENDENCY_MISSING" || + strings.Join(result[0].SelectedPackages, ",") != "demo-package" || result[0].DownloadBytes != 4<<10 || + result[0].DiskChangeBytes != 8<<10 { + t.Fatalf("mapped targets = %#v", result) + } +} + +func TestParseDNFPreviewOutputWithCLocaleTransaction(t *testing.T) { + output := `Last metadata expiration check: 0:00:01 ago on Tue 13 Oct 2026 02:00:00 PM CST. +Dependencies resolved. +================================================================================ + Package Architecture Version Repository Size +================================================================================ +Installing: + ocm-private noarch 1.0-1.oc9 ocmanager-private 6.0 k +Installing dependencies: + ocm-dependency x86_64 2:3.4-5.oc9 ocmanager-private 1.5 M +Installing weak dependencies: + ocm-weak noarch 1.1-2.oc9 ocmanager-private 2.0 k +Upgrading: + rpm x86_64 4.16.1.3-35.oc9 ocmanager-private 500 k +Replacing: + rpm-old x86_64 4.16.1.3-34.oc9 @System 480 k + +Transaction Summary +================================================================================ +Install 3 Packages +Upgrade 1 Package + +Total download size: 2.5 M +Installed size: 7.25 M +Operation aborted.` + got := parseDNFPreviewOutput(output, []string{"fallback-package"}) + if strings.Join(got.SelectedPackages, ",") != "ocm-private-1.0-1.oc9.noarch" || + strings.Join(got.DependencyPackages, ",") != + "ocm-dependency-2:3.4-5.oc9.x86_64,ocm-weak-1.1-2.oc9.noarch" || + strings.Join(got.UpgradePackages, ",") != "rpm-4.16.1.3-35.oc9.x86_64" || + strings.Join(got.ReplacementPackages, ",") != "rpm-old-4.16.1.3-34.oc9.x86_64" || + got.DownloadBytes != 2621440 || got.DiskChangeBytes != 7602176 { + t.Fatalf("parseDNFPreviewOutput() = %#v", got) + } +} + +func TestParseDNFPreviewOutputUsesRequestedPackagesAndFreedSpace(t *testing.T) { + got := parseDNFPreviewOutput("Dependencies resolved.\nFreed space: 1.5 GiB\nOperation aborted.", + []string{"zlib", "bash", "zlib"}) + if strings.Join(got.SelectedPackages, ",") != "bash,zlib" || + got.DiskChangeBytes != -(int64(3)<<29) { + t.Fatalf("parse fallback/freed space = %#v", got) + } +} + +func artifactScript(t *testing.T, artifact []byte) string { + t.Helper() + gzipReader, err := gzip.NewReader(bytes.NewReader(artifact)) + if err != nil { + t.Fatal(err) + } + defer gzipReader.Close() + tarReader := tar.NewReader(gzipReader) + header, err := tarReader.Next() + if err != nil || header.Name != "run.sh" { + t.Fatalf("artifact run.sh header = %#v, %v", header, err) + } + content, err := io.ReadAll(tarReader) + if err != nil { + t.Fatal(err) + } + return string(content) +} diff --git a/manager/backend/services/footstone/domain/repositoryScheduleService/acceptance_integration_test.go b/manager/backend/services/footstone/domain/repositoryScheduleService/acceptance_integration_test.go new file mode 100644 index 0000000000000000000000000000000000000000..df12804fda911a87b298efca0f6e23f2fd7a854c --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryScheduleService/acceptance_integration_test.go @@ -0,0 +1,134 @@ +//go:build integration + +package repositoryScheduleService + +import ( + "testing" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +type acceptanceScheduleTaskReader struct { + status string +} + +func (r *acceptanceScheduleTaskReader) DescribeInternal(id string) (*po.RepositoryTask, error) { + return &po.RepositoryTask{ID: id, Status: r.status, ErrorCode: "TEST_UPSTREAM_UNAVAILABLE", + ErrorMessage: "controlled schedule acceptance failure"}, nil +} + +func TestScheduleCalendarAndTerminalFailureRecoveryWithMySQL(t *testing.T) { + db := scheduleIntegrationDB(t) + defer db.Close() + const dailyID = "90909090-0000-4000-8000-000000000011" + const weeklyID = "90909090-0000-4000-8000-000000000012" + cleanupScheduleRows(t, db, dailyID, weeklyID) + defer cleanupScheduleRows(t, db, dailyID, weeklyID) + + now := time.Date(2026, 10, 19, 0, 0, 0, 0, time.UTC) // Monday. + enqueuer := &scheduleSyncEnqueuer{tasks: make(map[string]*po.RepositoryTask)} + reader := &acceptanceScheduleTaskReader{status: request.RepositoryTaskStatusFailed} + svc := New(db, scheduleRepositoryManager{}, enqueuer, reader) + svc.now = func() time.Time { return now } + ctx := schedulerContext("repository-schedule-acceptance") + daily, err := svc.Upsert(ctx, request.RepositorySyncScheduleSpec{RepositoryID: dailyID, + Cadence: request.RepositoryScheduleCadenceDaily, Timezone: "UTC", Hour: 3, FailureThreshold: 2}) + if err != nil { + t.Fatal(err) + } + weekly, err := svc.Upsert(ctx, request.RepositorySyncScheduleSpec{RepositoryID: weeklyID, + Cadence: request.RepositoryScheduleCadenceWeekly, Timezone: "UTC", + DayOfWeek: int(time.Tuesday), Hour: 4, FailureThreshold: 2}) + if err != nil { + t.Fatal(err) + } + assertTime := func(label string, got, want time.Time) { + t.Helper() + if !got.Equal(want) { + t.Fatalf("%s = %s, want %s", label, got, want) + } + } + assertTime("daily next", daily.NextRunAt, now.Add(3*time.Hour)) + assertTime("weekly next", weekly.NextRunAt, now.Add(28*time.Hour)) + if err = svc.Tick(); err != nil { + t.Fatal(err) + } + if enqueuer.calls.Load() != 0 { + t.Fatal("schedule ran before its calendar deadline") + } + + // Advance only the injected clock; persisted due times are never rewritten. + for day := 0; day < 2; day++ { + now = time.Date(2026, 10, 19+day, 3, 0, 0, 0, time.UTC) + if err = svc.Tick(); err != nil { + t.Fatal(err) + } + if err = svc.Tick(); err != nil { // Reconcile the real persisted run with a terminal task. + t.Fatal(err) + } + } + daily, err = svc.Describe(ctx, dailyID) + if err != nil { + t.Fatal(err) + } + if daily.Status != request.RepositoryScheduleStatusAutoPaused || daily.ConsecutiveFailures != 2 || + daily.LastErrorCode != "TEST_UPSTREAM_UNAVAILABLE" || daily.LastRunAt == nil { + t.Fatalf("terminal task failure did not auto-pause: %#v", daily) + } + assertTime("daily last", *daily.LastRunAt, now) + assertTime("daily next after failure", daily.NextRunAt, now.Add(24*time.Hour)) + if _, err = svc.SetStatus(ctx, weeklyID, request.RepositoryScheduleStatusPaused); err != nil { + t.Fatal(err) + } + before := enqueuer.calls.Load() + now = now.Add(8 * 24 * time.Hour) + if err = svc.Tick(); err != nil { + t.Fatal(err) + } + if enqueuer.calls.Load() != before { + t.Fatal("paused/auto-paused schedules generated repeated tasks") + } + daily, err = svc.SetStatus(ctx, dailyID, request.RepositoryScheduleStatusEnabled) + if err != nil { + t.Fatal(err) + } + if daily.ConsecutiveFailures != 0 || daily.LastErrorCode != "" || daily.LastErrorMessage != "" { + t.Fatalf("explicit enable failed to reset failure protection: %#v", daily) + } + if _, err = svc.SetStatus(ctx, dailyID, request.RepositoryScheduleStatusPaused); err != nil { + t.Fatal(err) + } + reader.status = request.RepositoryTaskStatusSuccess + weekly, err = svc.SetStatus(ctx, weeklyID, request.RepositoryScheduleStatusEnabled) + if err != nil { + t.Fatal(err) + } + if weekly.NextRunAt.Weekday() != time.Tuesday || weekly.NextRunAt.Hour() != 4 { + t.Fatalf("weekly calendar was lost after resume: %s", weekly.NextRunAt) + } + now = weekly.NextRunAt + if err = svc.Tick(); err != nil { + t.Fatal(err) + } + if err = svc.Tick(); err != nil { + t.Fatal(err) + } + weekly, err = svc.Describe(ctx, weeklyID) + if err != nil { + t.Fatal(err) + } + if weekly.LastRunAt == nil || weekly.ConsecutiveFailures != 0 { + t.Fatalf("weekly successful task was not reconciled: %#v", weekly) + } + assertTime("weekly last", *weekly.LastRunAt, now) + assertTime("weekly next", weekly.NextRunAt, now.Add(7*24*time.Hour)) + var run po.RepositoryScheduleRun + if err = db.DB().Where("repository_id = ?", weeklyID).Take(&run).Error; err != nil { + t.Fatal(err) + } + if run.Status != request.RepositoryScheduleRunSuccess || run.FinishedAt == nil { + t.Fatalf("weekly persisted run is not SUCCESS: %#v", run) + } +} diff --git a/manager/backend/services/footstone/domain/repositoryScheduleService/service.go b/manager/backend/services/footstone/domain/repositoryScheduleService/service.go new file mode 100644 index 0000000000000000000000000000000000000000..e435d2203dc1a5ec75fd7df508599b668b0517ae --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryScheduleService/service.go @@ -0,0 +1,370 @@ +package repositoryScheduleService + +import ( + "crypto/sha256" + "errors" + "fmt" + "strings" + "time" + + "github.com/google/uuid" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + ftpb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/footstone" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositoryschedulerepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryContentService" +) + +const schedulerBatchSize = 100 + +type repositoryManager interface { + Describe(entity.Context, string) (*po.SoftwareRepository, error) +} + +type syncEnqueuer interface { + SyncRepositoryScheduled(entity.Context, string, string) (*po.RepositoryTask, error) +} + +type taskReader interface { + DescribeInternal(string) (*po.RepositoryTask, error) +} + +type Service = *service + +type service struct { + db entity.SessionFactory + schedules repositoryschedulerepo.Repository + repositories repositoryManager + content syncEnqueuer + tasks taskReader + now func() time.Time +} + +func New(db *database.DBClient, repositories repositoryManager, content syncEnqueuer, + tasks taskReader, +) Service { + return &service{db: db, schedules: repositoryschedulerepo.New(db), repositories: repositories, + content: content, tasks: tasks, now: func() time.Time { return time.Now().UTC() }} +} + +func (s *service) SyncTables() error { return s.schedules.SyncTables() } + +func (s *service) Upsert(ctx entity.Context, spec request.RepositorySyncScheduleSpec) ( + value *po.RepositorySyncSchedule, err error, +) { + spec, location, err := normalizeSpec(spec) + if err != nil { + return nil, err + } + repository, err := s.repositories.Describe(ctx, spec.RepositoryID) + if err != nil { + return nil, err + } + if !repository.Enabled || strings.TrimSpace(repository.UpstreamURL) == "" { + return nil, repositoryContentService.ErrSyncUnavailable + } + now := s.now() + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + value, err = s.schedules.GetScheduleForUpdate(repository.ID, tx) + if errors.Is(err, repositoryschedulerepo.ErrScheduleNotFound) { + value = &po.RepositorySyncSchedule{ID: uuid.NewString(), RepositoryID: repository.ID, + ProjectID: repository.ProjectID, Status: request.RepositoryScheduleStatusEnabled, + CreatedBy: ctx.User.LoginName} + err = nil + } else if err != nil { + return nil, err + } + value.Cadence, value.Timezone = spec.Cadence, spec.Timezone + value.DayOfWeek, value.Hour, value.Minute = spec.DayOfWeek, spec.Hour, spec.Minute + value.FailureThreshold = spec.FailureThreshold + value.NextRunAt = nextRun(*value, now, location) + if value.Status == request.RepositoryScheduleStatusAutoPaused { + value.Status = request.RepositoryScheduleStatusEnabled + value.ConsecutiveFailures = 0 + value.LastErrorCode, value.LastErrorMessage = "", "" + } + if value.CreatedAt.IsZero() { + if err = s.schedules.CreateSchedule(value, tx); err != nil { + return nil, err + } + } else if err = s.schedules.UpdateSchedule(value, tx); err != nil { + return nil, err + } + return value, nil +} + +func (s *service) Describe(ctx entity.Context, repositoryID string) (*po.RepositorySyncSchedule, error) { + if _, err := s.repositories.Describe(ctx, strings.TrimSpace(repositoryID)); err != nil { + return nil, err + } + return s.schedules.GetScheduleByRepositoryID(strings.TrimSpace(repositoryID), nil) +} + +func (s *service) SetStatus(ctx entity.Context, repositoryID, status string) ( + value *po.RepositorySyncSchedule, err error, +) { + repositoryID = strings.TrimSpace(repositoryID) + if _, err = s.repositories.Describe(ctx, repositoryID); err != nil { + return nil, err + } + status = strings.ToUpper(strings.TrimSpace(status)) + if status != request.RepositoryScheduleStatusEnabled && status != request.RepositoryScheduleStatusPaused { + return nil, fmt.Errorf("schedule status must be ENABLED or PAUSED") + } + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + value, err = s.schedules.GetScheduleForUpdate(repositoryID, tx) + if err != nil { + return nil, err + } + value.Status = status + if status == request.RepositoryScheduleStatusEnabled { + location, locationErr := time.LoadLocation(value.Timezone) + if locationErr != nil { + return nil, locationErr + } + value.ConsecutiveFailures = 0 + value.LastErrorCode, value.LastErrorMessage = "", "" + value.NextRunAt = nextRun(*value, s.now(), location) + } + if err = s.schedules.UpdateSchedule(value, tx); err != nil { + return nil, err + } + return value, nil +} + +// Tick is safe to run in every control-plane instance. The schedule row lock +// and unique schedule_run_key make the operation distributed-idempotent. +func (s *service) Tick() error { + now := s.now() + if err := s.reconcile(now); err != nil { + return err + } + due, err := s.schedules.ListDue(now, schedulerBatchSize, nil) + if err != nil { + return err + } + for index := range due { + run, claimErr := s.claim(&due[index], now) + if claimErr != nil { + return claimErr + } + if run != nil { + if processErr := s.process(run); processErr != nil { + return processErr + } + } + } + return nil +} + +func (s *service) claim(candidate *po.RepositorySyncSchedule, now time.Time) ( + run *po.RepositoryScheduleRun, err error, +) { + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + schedule, err := s.schedules.GetScheduleForUpdate(candidate.RepositoryID, tx) + if err != nil { + return nil, err + } + if schedule.Status != request.RepositoryScheduleStatusEnabled || schedule.NextRunAt.After(now) { + return nil, nil + } + scheduledAt := schedule.NextRunAt.UTC() + digest := sha256.Sum256([]byte(schedule.ID + ":" + scheduledAt.Format(time.RFC3339Nano))) + run = &po.RepositoryScheduleRun{ID: uuid.NewString(), ScheduleID: schedule.ID, + RepositoryID: schedule.RepositoryID, ScheduledAt: scheduledAt, + ScheduleRunKey: fmt.Sprintf("%x", digest), Status: request.RepositoryScheduleRunPending} + if err = s.schedules.CreateRun(run, tx); err != nil { + if errors.Is(err, repositoryschedulerepo.ErrScheduleRunDuplicate) { + return nil, nil + } + return nil, err + } + location, err := time.LoadLocation(schedule.Timezone) + if err != nil { + return nil, err + } + schedule.LastRunAt = &scheduledAt + schedule.NextRunAt = nextRun(*schedule, scheduledAt.Add(time.Millisecond), location) + if err = s.schedules.UpdateSchedule(schedule, tx); err != nil { + return nil, err + } + return run, nil +} + +func (s *service) process(run *po.RepositoryScheduleRun) error { + if run.Status != request.RepositoryScheduleRunPending { + return nil + } + schedule, err := s.schedules.GetScheduleByRepositoryID(run.RepositoryID, nil) + if err != nil { + return err + } + ctx := schedulerContext(schedule.CreatedBy) + task, err := s.content.SyncRepositoryScheduled(ctx, run.RepositoryID, run.ScheduleRunKey) + if errors.Is(err, repositoryContentService.ErrSyncAlreadyRunning) { + return s.finishWithoutTask(run, request.RepositoryScheduleRunSkippedAlreadyRunning, "", "") + } + if err != nil { + return s.finishWithoutTask(run, request.RepositoryScheduleRunFailed, "SCHEDULE_ENQUEUE_FAILED", err.Error()) + } + run.TaskID = task.ID + run.Status = request.RepositoryScheduleRunTaskCreated + return s.schedules.UpdateRun(run, nil) +} + +func (s *service) reconcile(now time.Time) error { + runs, err := s.schedules.ListOpenRuns(schedulerBatchSize, nil) + if err != nil { + return err + } + for index := range runs { + run := &runs[index] + if run.Status == request.RepositoryScheduleRunPending { + if err = s.process(run); err != nil { + return err + } + continue + } + task, taskErr := s.tasks.DescribeInternal(run.TaskID) + if taskErr != nil { + continue + } + if !isTerminalTask(task.Status) { + continue + } + status := request.RepositoryScheduleRunFailed + if task.Status == request.RepositoryTaskStatusSuccess { + status = request.RepositoryScheduleRunSuccess + } + if err = s.finishRun(run, status, task.ErrorCode, task.ErrorMessage, now); err != nil { + return err + } + } + return nil +} + +func (s *service) finishWithoutTask(run *po.RepositoryScheduleRun, status, code, message string) error { + return s.finishRun(run, status, code, message, s.now()) +} + +func (s *service) finishRun(run *po.RepositoryScheduleRun, status, code, message string, now time.Time) ( + err error, +) { + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + stored, err := s.schedules.GetRunByKey(run.ScheduleRunKey, tx) + if err != nil { + return err + } + if stored.Status != request.RepositoryScheduleRunPending && + stored.Status != request.RepositoryScheduleRunTaskCreated { + return nil + } + stored.Status, stored.ErrorCode = status, truncate(code, 64) + stored.ErrorMessage = truncate(message, 2048) + stored.FinishedAt = timePointer(now) + if err = s.schedules.UpdateRun(stored, tx); err != nil { + return err + } + schedule, err := s.schedules.GetScheduleForUpdate(stored.RepositoryID, tx) + if err != nil { + return err + } + if status == request.RepositoryScheduleRunSuccess { + schedule.ConsecutiveFailures = 0 + schedule.LastErrorCode, schedule.LastErrorMessage = "", "" + } else if status == request.RepositoryScheduleRunFailed { + schedule.ConsecutiveFailures++ + schedule.LastErrorCode, schedule.LastErrorMessage = stored.ErrorCode, stored.ErrorMessage + if schedule.ConsecutiveFailures >= schedule.FailureThreshold { + schedule.Status = request.RepositoryScheduleStatusAutoPaused + } + } + return s.schedules.UpdateSchedule(schedule, tx) +} + +func normalizeSpec(spec request.RepositorySyncScheduleSpec) ( + request.RepositorySyncScheduleSpec, *time.Location, error, +) { + if _, err := uuid.Parse(strings.TrimSpace(spec.RepositoryID)); err != nil { + return spec, nil, fmt.Errorf("repository_id must be a UUID") + } + spec.RepositoryID = strings.TrimSpace(spec.RepositoryID) + spec.Cadence = strings.ToUpper(strings.TrimSpace(spec.Cadence)) + if spec.Cadence != request.RepositoryScheduleCadenceDaily && + spec.Cadence != request.RepositoryScheduleCadenceWeekly { + return spec, nil, fmt.Errorf("cadence must be DAILY or WEEKLY") + } + spec.Timezone = strings.TrimSpace(spec.Timezone) + if spec.Timezone == "" { + spec.Timezone = "UTC" + } + location, err := time.LoadLocation(spec.Timezone) + if err != nil { + return spec, nil, fmt.Errorf("invalid schedule timezone: %w", err) + } + if spec.DayOfWeek < 0 || spec.DayOfWeek > 6 || spec.Hour < 0 || spec.Hour > 23 || + spec.Minute < 0 || spec.Minute > 59 { + return spec, nil, fmt.Errorf("schedule day or time is invalid") + } + if spec.FailureThreshold == 0 { + spec.FailureThreshold = 3 + } + if spec.FailureThreshold < 1 || spec.FailureThreshold > 100 { + return spec, nil, fmt.Errorf("failure_threshold must be between 1 and 100") + } + return spec, location, nil +} + +func nextRun(schedule po.RepositorySyncSchedule, after time.Time, location *time.Location) time.Time { + local := after.In(location) + candidate := time.Date(local.Year(), local.Month(), local.Day(), schedule.Hour, schedule.Minute, 0, 0, location) + if schedule.Cadence == request.RepositoryScheduleCadenceWeekly { + days := (schedule.DayOfWeek - int(candidate.Weekday()) + 7) % 7 + candidate = candidate.AddDate(0, 0, days) + if !candidate.After(local) { + candidate = candidate.AddDate(0, 0, 7) + } + } else if !candidate.After(local) { + candidate = candidate.AddDate(0, 0, 1) + } + return candidate.UTC() +} + +func schedulerContext(loginName string) entity.Context { + ctx := entity.NewDefaultContext() + loginName = strings.TrimSpace(loginName) + if loginName == "" { + loginName = "repository-scheduler" + } + ctx.User = &ftpb.UserInfo{LoginName: loginName, + Roles: []*ftpb.ModuleRole{{RoleId: request.DefaultGlobalRoleName}}} + return ctx +} + +func isTerminalTask(status string) bool { + switch status { + case request.RepositoryTaskStatusSuccess, request.RepositoryTaskStatusFailed, + request.RepositoryTaskStatusCanceled, request.RepositoryTaskStatusPartialSuccess: + return true + default: + return false + } +} + +func truncate(value string, maximum int) string { + value = strings.TrimSpace(value) + runes := []rune(value) + if len(runes) > maximum { + return string(runes[:maximum]) + } + return value +} + +func timePointer(value time.Time) *time.Time { return &value } diff --git a/manager/backend/services/footstone/domain/repositoryScheduleService/service_integration_test.go b/manager/backend/services/footstone/domain/repositoryScheduleService/service_integration_test.go new file mode 100644 index 0000000000000000000000000000000000000000..efd3a0ddd7a54b7409e58b45c87b8cab97c502b6 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryScheduleService/service_integration_test.go @@ -0,0 +1,205 @@ +//go:build integration + +package repositoryScheduleService + +import ( + "errors" + "os" + "sync" + "sync/atomic" + "testing" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + ftpb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/footstone" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositoryschedulerepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryContentService" +) + +const ( + scheduleRepositoryConcurrent = "90909090-0000-4000-8000-000000000001" + scheduleRepositoryFailure = "90909090-0000-4000-8000-000000000002" + scheduleRepositorySkipped = "90909090-0000-4000-8000-000000000003" +) + +type scheduleRepositoryManager struct{} + +func (scheduleRepositoryManager) Describe(_ entity.Context, repositoryID string) (*po.SoftwareRepository, error) { + return &po.SoftwareRepository{ID: repositoryID, ProjectID: 920015, Enabled: true, + UpstreamURL: "https://repository.invalid/opencloudos/9/"}, nil +} + +type scheduleSyncEnqueuer struct { + err error + calls atomic.Int32 + mu sync.Mutex + tasks map[string]*po.RepositoryTask +} + +func (f *scheduleSyncEnqueuer) SyncRepositoryScheduled(_ entity.Context, repositoryID, key string) ( + *po.RepositoryTask, error, +) { + f.calls.Add(1) + if f.err != nil { + return nil, f.err + } + f.mu.Lock() + defer f.mu.Unlock() + if value := f.tasks[key]; value != nil { + return value, nil + } + value := &po.RepositoryTask{ID: "91919191-0000-4000-8000-000000000001", + RepositoryID: repositoryID, Status: request.RepositoryTaskStatusPending} + f.tasks[key] = value + return value, nil +} + +type scheduleTaskReader struct{} + +func (scheduleTaskReader) DescribeInternal(string) (*po.RepositoryTask, error) { + return nil, errors.New("task is still running") +} + +func TestDistributedScheduleClaimIsIdempotentWithMySQL(t *testing.T) { + db := scheduleIntegrationDB(t) + defer db.Close() + cleanupScheduleRows(t, db, scheduleRepositoryConcurrent) + defer cleanupScheduleRows(t, db, scheduleRepositoryConcurrent) + + now := time.Date(2026, 9, 20, 1, 0, 0, 0, time.UTC) + enqueuer := &scheduleSyncEnqueuer{tasks: make(map[string]*po.RepositoryTask)} + first := New(db, scheduleRepositoryManager{}, enqueuer, scheduleTaskReader{}) + second := New(db, scheduleRepositoryManager{}, enqueuer, scheduleTaskReader{}) + first.now, second.now = func() time.Time { return now }, func() time.Time { return now } + createDueSchedule(t, first, db, scheduleRepositoryConcurrent, now) + + var wait sync.WaitGroup + errorsSeen := make(chan error, 2) + for _, value := range []*service{first, second} { + wait.Add(1) + go func(instance *service) { + defer wait.Done() + errorsSeen <- instance.Tick() + }(value) + } + wait.Wait() + close(errorsSeen) + for err := range errorsSeen { + if err != nil { + t.Fatalf("concurrent Tick() error = %v", err) + } + } + + var runs int64 + if err := db.DB().Model(&po.RepositoryScheduleRun{}). + Where("repository_id = ?", scheduleRepositoryConcurrent).Count(&runs).Error; err != nil { + t.Fatal(err) + } + if runs != 1 || enqueuer.calls.Load() != 1 { + t.Fatalf("distributed claim runs/calls = %d/%d, want 1/1", runs, enqueuer.calls.Load()) + } +} + +func TestScheduleFailureAutoPausesAndRunningSyncIsSkippedWithMySQL(t *testing.T) { + db := scheduleIntegrationDB(t) + defer db.Close() + cleanupScheduleRows(t, db, scheduleRepositoryFailure, scheduleRepositorySkipped) + defer cleanupScheduleRows(t, db, scheduleRepositoryFailure, scheduleRepositorySkipped) + + now := time.Date(2026, 9, 20, 2, 0, 0, 0, time.UTC) + failing := &scheduleSyncEnqueuer{err: errors.New("upstream unavailable"), tasks: make(map[string]*po.RepositoryTask)} + service := New(db, scheduleRepositoryManager{}, failing, scheduleTaskReader{}) + service.now = func() time.Time { return now } + createDueSchedule(t, service, db, scheduleRepositoryFailure, now) + if err := db.DB().Model(&po.RepositorySyncSchedule{}). + Where("repository_id = ?", scheduleRepositoryFailure).Update("failure_threshold", 2).Error; err != nil { + t.Fatal(err) + } + for attempt := 0; attempt < 2; attempt++ { + if err := db.DB().Model(&po.RepositorySyncSchedule{}). + Where("repository_id = ?", scheduleRepositoryFailure). + Update("next_run_at", now.Add(-time.Duration(attempt+1)*time.Minute)).Error; err != nil { + t.Fatal(err) + } + if err := service.Tick(); err != nil { + t.Fatalf("failure Tick(%d) error = %v", attempt+1, err) + } + } + failed, err := repositoryschedulerepo.New(db).GetScheduleByRepositoryID(scheduleRepositoryFailure, nil) + if err != nil { + t.Fatal(err) + } + if failed.Status != request.RepositoryScheduleStatusAutoPaused || failed.ConsecutiveFailures != 2 || + failed.LastErrorCode != "SCHEDULE_ENQUEUE_FAILED" { + t.Fatalf("failed schedule = %#v", failed) + } + + skipping := &scheduleSyncEnqueuer{err: repositoryContentService.ErrSyncAlreadyRunning, + tasks: make(map[string]*po.RepositoryTask)} + skippedService := New(db, scheduleRepositoryManager{}, skipping, scheduleTaskReader{}) + skippedService.now = func() time.Time { return now } + createDueSchedule(t, skippedService, db, scheduleRepositorySkipped, now) + if err = skippedService.Tick(); err != nil { + t.Fatalf("skipped Tick() error = %v", err) + } + skipped, err := repositoryschedulerepo.New(db).GetScheduleByRepositoryID(scheduleRepositorySkipped, nil) + if err != nil { + t.Fatal(err) + } + if skipped.ConsecutiveFailures != 0 || skipped.Status != request.RepositoryScheduleStatusEnabled { + t.Fatalf("skipped schedule counted as failure = %#v", skipped) + } + var run po.RepositoryScheduleRun + if err = db.DB().Where("repository_id = ?", scheduleRepositorySkipped).Take(&run).Error; err != nil { + t.Fatal(err) + } + if run.Status != request.RepositoryScheduleRunSkippedAlreadyRunning { + t.Fatalf("skipped run status = %s", run.Status) + } +} + +func scheduleIntegrationDB(t *testing.T) *database.DBClient { + t.Helper() + dsn := os.Getenv("OCM_REPOSITORY_TEST_DSN") + if dsn == "" { + t.Skip("OCM_REPOSITORY_TEST_DSN is not set") + } + db, err := database.NewDBClient(config.AccessPoint{Source: dsn}) + if err != nil { + t.Fatal(err) + } + if err = repositoryschedulerepo.New(db).SyncTables(); err != nil { + db.Close() + t.Fatal(err) + } + return db +} + +func createDueSchedule(t *testing.T, service *service, db *database.DBClient, repositoryID string, now time.Time) { + t.Helper() + ctx := entity.NewDefaultContext() + ctx.User = &ftpb.UserInfo{LoginName: "repository-scheduler-integration", + Roles: []*ftpb.ModuleRole{{RoleId: request.DefaultGlobalRoleName}}} + if _, err := service.Upsert(ctx, request.RepositorySyncScheduleSpec{RepositoryID: repositoryID, + Cadence: request.RepositoryScheduleCadenceDaily, Timezone: "UTC", Hour: 3, FailureThreshold: 3}); err != nil { + t.Fatal(err) + } + if err := db.DB().Model(&po.RepositorySyncSchedule{}).Where("repository_id = ?", repositoryID). + Update("next_run_at", now.Add(-time.Minute)).Error; err != nil { + t.Fatal(err) + } +} + +func cleanupScheduleRows(t *testing.T, db *database.DBClient, repositoryIDs ...string) { + t.Helper() + if err := db.DB().Where("repository_id IN ?", repositoryIDs).Delete(&po.RepositoryScheduleRun{}).Error; err != nil { + t.Fatal(err) + } + if err := db.DB().Where("repository_id IN ?", repositoryIDs).Delete(&po.RepositorySyncSchedule{}).Error; err != nil { + t.Fatal(err) + } +} diff --git a/manager/backend/services/footstone/domain/repositoryScheduleService/service_test.go b/manager/backend/services/footstone/domain/repositoryScheduleService/service_test.go new file mode 100644 index 0000000000000000000000000000000000000000..b8a641601eff7314e11cc7e3c6b535fbc4cb6412 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryScheduleService/service_test.go @@ -0,0 +1,40 @@ +package repositoryScheduleService + +import ( + "testing" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +func TestNextRunDailyAndWeekly(t *testing.T) { + location, err := time.LoadLocation("Asia/Shanghai") + if err != nil { + t.Fatal(err) + } + after := time.Date(2026, 9, 19, 3, 0, 0, 0, location) + daily := po.RepositorySyncSchedule{Cadence: request.RepositoryScheduleCadenceDaily, Hour: 2} + if got := nextRun(daily, after, location).In(location); got.Day() != 20 || got.Hour() != 2 { + t.Fatalf("next daily run = %s", got) + } + weekly := po.RepositorySyncSchedule{Cadence: request.RepositoryScheduleCadenceWeekly, + DayOfWeek: int(time.Monday), Hour: 1, Minute: 30} + got := nextRun(weekly, after, location).In(location) + if got.Weekday() != time.Monday || got.Day() != 21 || got.Hour() != 1 || got.Minute() != 30 { + t.Fatalf("next weekly run = %s", got) + } +} + +func TestNormalizeScheduleSpec(t *testing.T) { + value, _, err := normalizeSpec(request.RepositorySyncScheduleSpec{ + RepositoryID: "11111111-1111-4111-8111-111111111111", Cadence: "daily", + Timezone: "UTC", Hour: 3, Minute: 15, + }) + if err != nil { + t.Fatal(err) + } + if value.Cadence != request.RepositoryScheduleCadenceDaily || value.FailureThreshold != 3 { + t.Fatalf("normalizeSpec() = %#v", value) + } +} diff --git a/manager/backend/services/footstone/domain/repositoryService/service.go b/manager/backend/services/footstone/domain/repositoryService/service.go new file mode 100644 index 0000000000000000000000000000000000000000..9a325babe5df29b9fdcc3c12fe96b2d3aec93ee6 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryService/service.go @@ -0,0 +1,627 @@ +package repositoryService + +import ( + "errors" + "fmt" + "net/url" + "regexp" + "strings" + "time" + "unicode" + "unicode/utf8" + + "github.com/google/uuid" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/infrarepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorycontentrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositoryrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorystoragenoderepo" +) + +const maxBatchRepositories = 100 + +var ( + ErrUnauthenticated = errors.New("authenticated user is required") + ErrForbidden = errors.New("user cannot manage repositories in this project") + ErrProjectChange = errors.New("repository project cannot be changed") + ErrDeleteConfirmation = errors.New("repository deletion requires explicit confirmation") + ErrRepositoryHasContent = errors.New("repository still contains active packages, unfinished uploads, GPG keys, or running tasks; cleanup is required before deletion") + ErrStorageNodeProjectMismatch = errors.New("repository storage node belongs to another project") + ErrStorageNodeDisabled = errors.New("repository storage node is disabled") + ErrStorageNodeChangeRequiresMigration = errors.New("repository storage node change requires a MIGRATE_REPOSITORY task") + ErrSystemManagedRepository = errors.New("system-managed repository cannot be changed through the standard repository API") + namePattern = regexp.MustCompile(`^[A-Za-z0-9._+-]+$`) +) + +type projectReader interface { + DescribeProject(entity.Context, request.DescribeProjectRequest, entity.Session) (request.DescribeProjectResponse, error) +} + +type Service = *service + +type service struct { + db entity.SessionFactory + repositories repositoryrepo.Repository + nodes storageNodeReader + projects projectReader + metadataKeys metadataSigningKeyReader +} + +type storageNodeReader interface { + GetNodeByID(string, entity.Session) (*po.RepositoryStorageNode, error) +} + +type metadataSigningKeyReader interface { + GetMetadataSigningKey(string, string, entity.Session) (*po.RepositoryMetadataSigningKey, error) +} + +type BatchResult struct { + InputIndex int + Operation string + Repository *po.SoftwareRepository +} + +func New(db *database.DBClient) Service { + return newService(db, repositoryrepo.New(db), repositorystoragenoderepo.New(db), + infrarepo.NewInfrastructureRepository(db), repositorycontentrepo.New(db)) +} + +func newService(db entity.SessionFactory, repositories repositoryrepo.Repository, nodes storageNodeReader, + projects projectReader, metadataKeys ...metadataSigningKeyReader, +) Service { + value := &service{db: db, repositories: repositories, nodes: nodes, projects: projects} + if len(metadataKeys) > 0 { + value.metadataKeys = metadataKeys[0] + } + return value +} + +func (s *service) SyncTables() error { + return s.repositories.SyncTables() +} + +func (s *service) Create(ctx entity.Context, spec request.SoftwareRepositorySpec) ( + value *po.SoftwareRepository, err error, +) { + spec, err = NormalizeSpec(spec) + if err != nil { + return nil, err + } + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + if err = s.authorizeProject(ctx, spec.ProjectID, tx); err != nil { + return nil, err + } + node, err := s.loadStorageNode(spec.PrimaryStorageNodeID, spec.ProjectID, tx) + if err != nil { + return nil, err + } + if err = s.validateMetadataSigningKey(node.ID, spec.MetadataSigningKeyRef, tx); err != nil { + return nil, err + } + value, err = newSoftwareRepository(ctx, spec, node) + if err != nil { + return nil, err + } + if err = s.repositories.Create(value, tx); err != nil { + return nil, err + } + return value, nil +} + +func (s *service) Update(ctx entity.Context, id string, spec request.SoftwareRepositorySpec) ( + value *po.SoftwareRepository, err error, +) { + if err = validateRepositoryID(id); err != nil { + return nil, err + } + spec, err = NormalizeSpec(spec) + if err != nil { + return nil, err + } + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + value, err = s.repositories.GetByID(id, tx) + if err != nil { + return nil, err + } + if value.SystemManaged { + return nil, ErrSystemManagedRepository + } + if spec.ProjectID != value.ProjectID { + return nil, ErrProjectChange + } + if err = s.authorizeProject(ctx, value.ProjectID, tx); err != nil { + return nil, err + } + node, err := s.loadStorageNode(spec.PrimaryStorageNodeID, spec.ProjectID, tx) + if err != nil { + return nil, err + } + if err = s.validateMetadataSigningKey(node.ID, spec.MetadataSigningKeyRef, tx); err != nil { + return nil, err + } + if value.PrimaryStorageNodeID != "" && value.PrimaryStorageNodeID != spec.PrimaryStorageNodeID { + hasContent := value.ActiveRevisionID != "" + if !hasContent { + hasContent, err = repositoryHasBlockingContent(tx, value) + if err != nil { + return nil, err + } + } + if hasContent { + return nil, ErrStorageNodeChangeRequiresMigration + } + } + if err = applySpec(value, spec, node); err != nil { + return nil, err + } + if err = s.repositories.Update(value, tx); err != nil { + return nil, err + } + return s.repositories.GetByID(id, tx) +} + +func (s *service) Describe(ctx entity.Context, id string) (_ *po.SoftwareRepository, err error) { + if err = validateRepositoryID(id); err != nil { + return nil, err + } + value, err := s.repositories.GetByID(id, nil) + if err != nil { + return nil, err + } + if err = s.authorizeProject(ctx, value.ProjectID, nil); err != nil { + return nil, err + } + return value, nil +} + +// DescribeInternal is reserved for the mTLS repository-node control plane. +func (s *service) DescribeInternal(id string) (*po.SoftwareRepository, error) { + if err := validateRepositoryID(id); err != nil { + return nil, err + } + return s.repositories.GetByID(id, nil) +} + +func (s *service) List(ctx entity.Context, filter request.ListSoftwareRepositoriesRequest) ( + []po.SoftwareRepository, request.PageResponse, error, +) { + if filter.ProjectID <= 0 { + return nil, request.PageResponse{}, fmt.Errorf("project_id must be greater than zero") + } + if err := s.authorizeProject(ctx, filter.ProjectID, nil); err != nil { + return nil, request.PageResponse{}, err + } + filter.SystemProduct = strings.TrimSpace(filter.SystemProduct) + filter.ProductVersion = strings.TrimSpace(filter.ProductVersion) + filter.Keyword = strings.TrimSpace(filter.Keyword) + filter.Architecture = normalizeArchitecture(filter.Architecture) + filter.Type = strings.ToUpper(strings.TrimSpace(filter.Type)) + if filter.Architecture != "" { + switch filter.Architecture { + case request.RepositoryArchitectureX8664, request.RepositoryArchitectureARM64, + request.RepositoryArchitectureNoarch: + default: + return nil, request.PageResponse{}, fmt.Errorf("unsupported repository architecture %q", filter.Architecture) + } + } + if filter.Type != "" { + switch filter.Type { + case request.RepositoryTypeBaseline, request.RepositoryTypeUpdate, request.RepositoryTypePrivate: + default: + return nil, request.PageResponse{}, fmt.Errorf("unsupported repository type %q", filter.Type) + } + } + if filter.PageRequest.PageNumber <= 0 { + filter.PageRequest.PageNumber = 1 + } + if filter.PageRequest.PageSize <= 0 { + filter.PageRequest.PageSize = 50 + } + if filter.PageRequest.PageSize > 200 { + return nil, request.PageResponse{}, fmt.Errorf("page_size cannot exceed 200") + } + return s.repositories.List(filter, nil) +} + +func (s *service) Delete(ctx entity.Context, id string, confirm bool) (err error) { + if !confirm { + return ErrDeleteConfirmation + } + if err = validateRepositoryID(id); err != nil { + return err + } + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + value, err := s.repositories.GetByID(id, tx) + if err != nil { + return err + } + if value.SystemManaged { + return ErrSystemManagedRepository + } + if err = s.authorizeProject(ctx, value.ProjectID, tx); err != nil { + return err + } + hasContent, checkErr := repositoryHasBlockingContent(tx, value) + if checkErr != nil { + return checkErr + } + if hasContent { + return ErrRepositoryHasContent + } + if err = tx.Where("repository_id = ?", value.ID).Delete(&po.RepositoryQuarantineItem{}).Error; err != nil { + return err + } + return s.repositories.Delete(id, tx) +} + +func repositoryHasBlockingContent(session entity.Session, repository *po.SoftwareRepository) (bool, error) { + if repository.ActiveRevisionID != "" { + blocked, err := modelExists(session, new(po.RepositoryPackage), + "repository_id = ? AND revision_id = ?", repository.ID, repository.ActiveRevisionID) + if err != nil || blocked { + return blocked, err + } + } + checks := []struct { + model interface{} + query string + args []interface{} + }{ + {new(po.RepositoryUpload), "repository_id = ? AND status IN ? AND (expires_at IS NULL OR expires_at > ?)", + []interface{}{repository.ID, + []string{request.RepositoryUploadStatusReceiving, request.RepositoryUploadStatusReady, + request.RepositoryUploadStatusProcessing, request.RepositoryUploadStatusPaused}, time.Now().UTC()}}, + {new(po.RepositoryGPGKey), "repository_id = ?", []interface{}{repository.ID}}, + {new(po.RepositoryTask), "repository_id = ? AND status IN ?", []interface{}{repository.ID, + []string{request.RepositoryTaskStatusPending, request.RepositoryTaskStatusRunning, + request.RepositoryTaskStatusCancelRequested}}}, + } + for _, check := range checks { + blocked, err := modelExists(session, check.model, check.query, check.args...) + if err != nil || blocked { + return blocked, err + } + } + return false, nil +} + +func modelExists(session entity.Session, model interface{}, query string, args ...interface{}) (bool, error) { + var count int64 + if err := session.Model(model).Where(query, args...).Count(&count).Error; err != nil { + return false, err + } + return count > 0, nil +} + +func (s *service) BatchApply(ctx entity.Context, items []request.BatchApplySoftwareRepositoryItem) ( + results []BatchResult, err error, +) { + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + return s.BatchApplyInSession(ctx, items, tx) +} + +// BatchApplyInSession applies a repository batch in the caller's transaction. +// It is used when a higher-level operation must atomically persist repositories +// together with its preview and audit records. +func (s *service) BatchApplyInSession(ctx entity.Context, items []request.BatchApplySoftwareRepositoryItem, + session entity.Session, +) (results []BatchResult, err error) { + if len(items) == 0 || len(items) > maxBatchRepositories { + return nil, fmt.Errorf("batch items must contain between 1 and %d repositories", maxBatchRepositories) + } + if session == nil { + return nil, errors.New("batch apply session is required") + } + authorizedProjects := make(map[int64]struct{}) + results = make([]BatchResult, 0, len(items)) + + for index, item := range items { + item.Spec, err = NormalizeSpec(item.Spec) + if err != nil { + return nil, fmt.Errorf("batch item %d: %w", index, err) + } + if _, ok := authorizedProjects[item.Spec.ProjectID]; !ok { + if err = s.authorizeProject(ctx, item.Spec.ProjectID, session); err != nil { + return nil, fmt.Errorf("batch item %d: %w", index, err) + } + authorizedProjects[item.Spec.ProjectID] = struct{}{} + } + + var value *po.SoftwareRepository + node, nodeErr := s.loadStorageNode(item.Spec.PrimaryStorageNodeID, item.Spec.ProjectID, session) + if nodeErr != nil { + return nil, fmt.Errorf("batch item %d: %w", index, nodeErr) + } + if keyErr := s.validateMetadataSigningKey(node.ID, item.Spec.MetadataSigningKeyRef, session); keyErr != nil { + return nil, fmt.Errorf("batch item %d: %w", index, keyErr) + } + operation := "CREATE" + if strings.TrimSpace(item.RepositoryID) == "" { + value, err = newSoftwareRepository(ctx, item.Spec, node) + if err == nil { + err = s.repositories.Create(value, session) + } + } else { + operation = "UPDATE" + if err = validateRepositoryID(item.RepositoryID); err == nil { + value, err = s.repositories.GetByID(item.RepositoryID, session) + } + if err == nil && value.SystemManaged { + err = ErrSystemManagedRepository + } + if err == nil && value.ProjectID != item.Spec.ProjectID { + err = ErrProjectChange + } + if err == nil && value.PrimaryStorageNodeID != "" && + value.PrimaryStorageNodeID != item.Spec.PrimaryStorageNodeID { + err = ErrStorageNodeChangeRequiresMigration + } + if err == nil { + err = applySpec(value, item.Spec, node) + } + if err == nil { + err = s.repositories.Update(value, session) + } + if err == nil { + value, err = s.repositories.GetByID(item.RepositoryID, session) + } + } + if err != nil { + return nil, fmt.Errorf("batch item %d: %w", index, err) + } + results = append(results, BatchResult{ + InputIndex: index, + Operation: operation, + Repository: value, + }) + } + return results, nil +} + +func (s *service) authorizeProject(ctx entity.Context, projectID int64, session entity.Session) error { + if ctx.User == nil || strings.TrimSpace(ctx.User.LoginName) == "" { + return ErrUnauthenticated + } + project, err := s.projects.DescribeProject(ctx, request.DescribeProjectRequest{ID: projectID}, session) + if err != nil { + return fmt.Errorf("project %d not found: %w", projectID, err) + } + for _, role := range ctx.User.Roles { + if role.RoleId == request.DefaultGlobalRoleName { + return nil + } + } + if project.Project.Creator != ctx.User.LoginName { + return ErrForbidden + } + return nil +} + +func NormalizeSpec(spec request.SoftwareRepositorySpec) (request.SoftwareRepositorySpec, error) { + if spec.ProjectID <= 0 { + return spec, fmt.Errorf("project_id must be greater than zero") + } + var err error + if spec.SystemProduct, err = normalizeLabel("system_product", spec.SystemProduct, 128); err != nil { + return spec, err + } + if spec.ProductVersion, err = normalizeLabel("product_version", spec.ProductVersion, 64); err != nil { + return spec, err + } + spec.Name = strings.TrimSpace(spec.Name) + if spec.Name == "" || utf8.RuneCountInString(spec.Name) > 128 || !namePattern.MatchString(spec.Name) { + return spec, fmt.Errorf("name must use 1-128 ASCII letters, digits, '.', '_', '+', or '-'") + } + spec.Description = strings.TrimSpace(spec.Description) + if utf8.RuneCountInString(spec.Description) > 1024 || containsControl(spec.Description) { + return spec, fmt.Errorf("description is invalid or exceeds 1024 characters") + } + spec.Architecture = normalizeArchitecture(spec.Architecture) + switch spec.Architecture { + case request.RepositoryArchitectureX8664, request.RepositoryArchitectureARM64, + request.RepositoryArchitectureNoarch: + default: + return spec, fmt.Errorf("unsupported repository architecture %q", spec.Architecture) + } + spec.Type = strings.ToUpper(strings.TrimSpace(spec.Type)) + switch spec.Type { + case request.RepositoryTypeBaseline, request.RepositoryTypeUpdate, request.RepositoryTypePrivate: + default: + return spec, fmt.Errorf("unsupported repository type %q", spec.Type) + } + if spec.AllowUpload && spec.Type != request.RepositoryTypePrivate { + return spec, fmt.Errorf("allow_upload is only valid for PRIVATE repositories") + } + if spec.UpstreamURL, err = normalizeUpstreamURL(spec.UpstreamURL); err != nil { + return spec, err + } + spec.GPGPolicy = strings.ToUpper(strings.TrimSpace(spec.GPGPolicy)) + if spec.GPGPolicy == "" { + spec.GPGPolicy = request.RepositoryGPGDisabled + } + switch spec.GPGPolicy { + case request.RepositoryGPGDisabled, request.RepositoryGPGAudit, request.RepositoryGPGEnforce, + request.RepositoryGPGVerifyPackage: + default: + return spec, fmt.Errorf("unsupported GPG policy %q", spec.GPGPolicy) + } + if spec.IncludePackages, err = normalizePatterns("include_packages", spec.IncludePackages); err != nil { + return spec, err + } + if spec.ExcludePackages, err = normalizePatterns("exclude_packages", spec.ExcludePackages); err != nil { + return spec, err + } + if _, err = uuid.Parse(strings.TrimSpace(spec.PrimaryStorageNodeID)); err != nil { + return spec, fmt.Errorf("primary_storage_node_id must be a UUID") + } + spec.PrimaryStorageNodeID = strings.TrimSpace(spec.PrimaryStorageNodeID) + spec.SyncMode = strings.ToUpper(strings.TrimSpace(spec.SyncMode)) + if spec.SyncMode == "" { + spec.SyncMode = request.RepositorySyncModeMirror + } + switch spec.SyncMode { + case request.RepositorySyncModeMirror, request.RepositorySyncModeAppend: + default: + return spec, fmt.Errorf("unsupported repository sync mode %q", spec.SyncMode) + } + if spec.BandwidthLimitBytes < 0 { + return spec, fmt.Errorf("bandwidth_limit_bytes cannot be negative") + } + if spec.MaxParallelDownloads == 0 { + spec.MaxParallelDownloads = 3 + } + if spec.MaxParallelDownloads < 1 || spec.MaxParallelDownloads > 20 { + return spec, fmt.Errorf("max_parallel_downloads must be between 1 and 20") + } + spec.MetadataSigningKeyRef = strings.TrimSpace(spec.MetadataSigningKeyRef) + if len(spec.MetadataSigningKeyRef) > 128 || + (spec.MetadataSigningKeyRef != "" && !namePattern.MatchString(spec.MetadataSigningKeyRef)) { + return spec, fmt.Errorf("metadata_signing_key_ref is invalid") + } + return spec, nil +} + +func newSoftwareRepository(ctx entity.Context, spec request.SoftwareRepositorySpec, + node *po.RepositoryStorageNode, +) (*po.SoftwareRepository, error) { + publishKey := strings.ReplaceAll(uuid.NewString(), "-", "") + value := &po.SoftwareRepository{ + ID: uuid.NewString(), + ProjectID: spec.ProjectID, + PublishKey: publishKey, + StorageLocation: "public/" + publishKey, + Status: request.RepositoryStatusEmpty, + Creator: ctx.User.LoginName, + } + if err := applySpec(value, spec, node); err != nil { + return nil, err + } + return value, nil +} + +func applySpec(value *po.SoftwareRepository, spec request.SoftwareRepositorySpec, + node *po.RepositoryStorageNode, +) error { + value.SystemProduct = spec.SystemProduct + value.ProductVersion = spec.ProductVersion + value.Name = spec.Name + value.Description = spec.Description + value.Architecture = spec.Architecture + value.Type = spec.Type + value.UpstreamURL = spec.UpstreamURL + value.AllowUpload = spec.AllowUpload + value.GPGPolicy = spec.GPGPolicy + value.Enabled = spec.Enabled + value.PrimaryStorageNodeID = spec.PrimaryStorageNodeID + value.PublishBaseURLSnapshot = strings.TrimRight(node.PublishBaseURL, "/") + value.SyncMode = spec.SyncMode + value.BandwidthLimitBytes = spec.BandwidthLimitBytes + value.MaxParallelDownloads = spec.MaxParallelDownloads + value.MetadataSigningKeyRef = spec.MetadataSigningKeyRef + return value.SetPackageFilters(spec.IncludePackages, spec.ExcludePackages) +} + +func (s *service) loadStorageNode(id string, projectID int64, session entity.Session) ( + *po.RepositoryStorageNode, error, +) { + node, err := s.nodes.GetNodeByID(id, session) + if err != nil { + return nil, err + } + if node.ProjectID != projectID { + return nil, ErrStorageNodeProjectMismatch + } + if !node.Enabled || node.Status == request.RepositoryStorageNodeStatusDisabled { + return nil, ErrStorageNodeDisabled + } + return node, nil +} + +func (s *service) validateMetadataSigningKey(nodeID, keyRef string, session entity.Session) error { + keyRef = strings.TrimSpace(keyRef) + if keyRef == "" { + return nil + } + if s.metadataKeys == nil { + return errors.New("metadata signing key inventory is unavailable") + } + key, err := s.metadataKeys.GetMetadataSigningKey(nodeID, keyRef, session) + if err != nil { + return fmt.Errorf("metadata signing key %q is not reported by storage node: %w", keyRef, err) + } + if !strings.EqualFold(key.Status, "AVAILABLE") || strings.TrimSpace(key.Fingerprint) == "" || + strings.TrimSpace(key.PublicKeyName) == "" { + return fmt.Errorf("metadata signing key %q is not available on storage node", keyRef) + } + return nil +} + +func validateRepositoryID(id string) error { + if _, err := uuid.Parse(strings.TrimSpace(id)); err != nil { + return fmt.Errorf("invalid repository_id: %w", err) + } + return nil +} + +func normalizeLabel(field, value string, maxLength int) (string, error) { + value = strings.TrimSpace(value) + if value == "" || utf8.RuneCountInString(value) > maxLength || containsControl(value) || strings.ContainsAny(value, `/\\`) { + return value, fmt.Errorf("%s is invalid or exceeds %d characters", field, maxLength) + } + return value, nil +} + +func normalizeArchitecture(value string) string { + value = strings.ToLower(strings.TrimSpace(value)) + if value == "arm64" { + return request.RepositoryArchitectureARM64 + } + return value +} + +func normalizeUpstreamURL(value string) (string, error) { + value = strings.TrimSpace(value) + if value == "" { + return "", nil + } + parsed, err := url.Parse(value) + if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" { + return value, fmt.Errorf("upstream_url must be an absolute HTTP(S) URL") + } + if parsed.User != nil || parsed.Fragment != "" { + return value, fmt.Errorf("upstream_url cannot contain credentials or a fragment") + } + return parsed.String(), nil +} + +func normalizePatterns(field string, values []string) ([]string, error) { + if len(values) > 100 { + return nil, fmt.Errorf("%s cannot contain more than 100 entries", field) + } + result := make([]string, 0, len(values)) + seen := make(map[string]struct{}, len(values)) + for _, value := range values { + value = strings.TrimSpace(value) + if value == "" || utf8.RuneCountInString(value) > 256 || containsControl(value) { + return nil, fmt.Errorf("%s contains an invalid package pattern", field) + } + if _, exists := seen[value]; exists { + continue + } + seen[value] = struct{}{} + result = append(result, value) + } + return result, nil +} + +func containsControl(value string) bool { + return strings.IndexFunc(value, unicode.IsControl) >= 0 +} diff --git a/manager/backend/services/footstone/domain/repositoryService/service_integration_test.go b/manager/backend/services/footstone/domain/repositoryService/service_integration_test.go new file mode 100644 index 0000000000000000000000000000000000000000..96f95f0887347094a434d0a5836ae2efacf9c1cc --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryService/service_integration_test.go @@ -0,0 +1,206 @@ +//go:build integration + +package repositoryService + +import ( + "errors" + "os" + "strings" + "testing" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + ftpb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/footstone" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/infrarepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositoryrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorystoragenoderepo" +) + +const integrationProjectID int64 = 920001 +const integrationStorageNodeID = "92000100-0000-4000-8000-000000000001" + +func TestRepositoryCRUDAndBatchWithMySQL(t *testing.T) { + dsn := os.Getenv("OCM_REPOSITORY_TEST_DSN") + if dsn == "" { + t.Skip("OCM_REPOSITORY_TEST_DSN is not set") + } + db, err := database.NewDBClient(config.AccessPoint{Source: dsn}) + if err != nil { + t.Fatalf("connect test MySQL: %v", err) + } + defer db.Close() + + if err = db.DB().AutoMigrate(&po.Project{}); err != nil { + t.Fatalf("create disposable project table: %v", err) + } + db.DB().Where("project_id = ?", integrationProjectID).Delete(&po.SoftwareRepository{}) + db.DB().Where("id = ?", integrationProjectID).Delete(&po.Project{}) + project := po.Project{ + Base: po.Base{ID: integrationProjectID}, + Name: "repository-phase1-integration", + EnName: "repository-phase1-integration", + Creator: "repository-admin", + } + if err = db.DB().Create(&project).Error; err != nil { + t.Fatalf("create disposable project: %v", err) + } + defer func() { + db.DB().Where("project_id = ?", integrationProjectID).Delete(&po.SoftwareRepository{}) + db.DB().Where("id = ?", integrationProjectID).Delete(&po.Project{}) + }() + + nodes := repositorystoragenoderepo.New(db) + if err = nodes.SyncTables(); err != nil { + t.Fatalf("create repository node tables: %v", err) + } + db.DB().Where("id = ?", integrationStorageNodeID).Delete(&po.RepositoryStorageNode{}) + if err = db.DB().Create(&po.RepositoryStorageNode{ID: integrationStorageNodeID, ProjectID: integrationProjectID, + Name: "integration-node", ControlBaseURL: "https://127.0.0.1:19443", + PublishBaseURL: "http://127.0.0.1:18090", Status: request.RepositoryStorageNodeStatusOnline, + Enabled: true, CreatedBy: "repository-admin"}).Error; err != nil { + t.Fatalf("create integration storage node: %v", err) + } + defer db.DB().Where("id = ?", integrationStorageNodeID).Delete(&po.RepositoryStorageNode{}) + service := newService(db, repositoryrepo.New(db), nodes, infrarepo.NewInfrastructureRepository(db)) + ctx := integrationContext("repository-admin", false) + spec := integrationSpec("private-apps") + + created, err := service.Create(ctx, spec) + if err != nil { + t.Fatalf("Create() error = %v", err) + } + if created.ID == "" || created.PublishKey == "" || created.StorageLocation != "public/"+created.PublishKey { + t.Fatalf("Create() generated identity/path = %#v", created) + } + if _, err = service.Create(ctx, spec); !errors.Is(err, repositoryrepo.ErrRepositoryDuplicate) { + t.Fatalf("duplicate Create() error = %v", err) + } + + described, err := service.Describe(ctx, created.ID) + if err != nil || described.Name != spec.Name { + t.Fatalf("Describe() value/error = %#v/%v", described, err) + } + if _, err = service.Describe(integrationContext("other-user", false), created.ID); !errors.Is(err, ErrForbidden) { + t.Fatalf("unauthorized Describe() error = %v", err) + } + + values, page, err := service.List(ctx, request.ListSoftwareRepositoriesRequest{ + ProjectID: integrationProjectID, + SystemProduct: spec.SystemProduct, + ProductVersion: spec.ProductVersion, + Architecture: spec.Architecture, + Type: spec.Type, + }) + if err != nil || len(values) != 1 || page.Count != 1 { + t.Fatalf("List() values/page/error = %d/%#v/%v", len(values), page, err) + } + + spec.Description = "updated description" + updated, err := service.Update(ctx, created.ID, spec) + if err != nil || updated.Description != spec.Description || updated.UpdatedAt.IsZero() { + t.Fatalf("Update() value/error = %#v/%v", updated, err) + } + if _, err = service.Update(ctx, created.ID, spec); err != nil { + t.Fatalf("idempotent Update() error = %v", err) + } + + rollbackName := "batch-must-roll-back" + rollbackUpdate := spec + rollbackUpdate.Description = "this update must roll back" + _, err = service.BatchApply(ctx, []request.BatchApplySoftwareRepositoryItem{ + {RepositoryID: created.ID, Spec: rollbackUpdate}, + {Spec: integrationSpec(rollbackName)}, + {Spec: integrationSpec(spec.Name)}, + }) + if !errors.Is(err, repositoryrepo.ErrRepositoryDuplicate) { + t.Fatalf("BatchApply() rollback error = %v", err) + } + var rollbackCount int64 + if err = db.DB().Model(&po.SoftwareRepository{}). + Where("project_id = ? AND name = ?", integrationProjectID, rollbackName). + Count(&rollbackCount).Error; err != nil || rollbackCount != 0 { + t.Fatalf("BatchApply() rollback count/error = %d/%v", rollbackCount, err) + } + described, err = service.Describe(ctx, created.ID) + if err != nil || described.Description != spec.Description { + t.Fatalf("BatchApply() rolled-back update value/error = %#v/%v", described, err) + } + + batchResults, err := service.BatchApply(ctx, []request.BatchApplySoftwareRepositoryItem{ + {Spec: integrationSpec("batch-one")}, + {Spec: integrationSpec("batch-two")}, + }) + if err != nil || len(batchResults) != 2 { + t.Fatalf("BatchApply() success results/error = %d/%v", len(batchResults), err) + } + batchUpdate := integrationSpec("batch-one") + batchUpdate.Description = "updated in atomic batch" + batchResults, err = service.BatchApply(ctx, []request.BatchApplySoftwareRepositoryItem{ + {RepositoryID: batchResults[0].Repository.ID, Spec: batchUpdate}, + {Spec: integrationSpec("batch-three")}, + }) + if err != nil || len(batchResults) != 2 || batchResults[0].Operation != "UPDATE" || + batchResults[0].Repository.Description != batchUpdate.Description || batchResults[1].Operation != "CREATE" { + t.Fatalf("BatchApply() mixed update/create results/error = %#v/%v", batchResults, err) + } + + if err = service.Delete(ctx, created.ID, false); !errors.Is(err, ErrDeleteConfirmation) { + t.Fatalf("Delete() without confirmation error = %v", err) + } + activeRevisionID := "11111111-1111-1111-1111-111111111111" + if err = db.DB().Model(&po.SoftwareRepository{}).Where("id = ?", created.ID). + Update("active_revision_id", activeRevisionID).Error; err != nil { + t.Fatalf("set active revision: %v", err) + } + if err = db.DB().Create(&po.RepositoryPackage{ID: "22222222-2222-4222-8222-222222222222", + RepositoryID: created.ID, RevisionID: activeRevisionID, Name: "busy", Version: "1.0", + Release: "1", Architecture: request.RepositoryArchitectureX8664, + NEVRA: "busy-0:1.0-1.x86_64", FileName: "busy-1.0-1.x86_64.rpm", + RelativePath: "packages/busy-1.0-1.x86_64.rpm", SHA256: strings.Repeat("a", 64), + Source: request.RepositoryPackageSourceUpload}).Error; err != nil { + t.Fatalf("create active package: %v", err) + } + if err = service.Delete(ctx, created.ID, true); !errors.Is(err, ErrRepositoryHasContent) { + t.Fatalf("Delete() active repository error = %v", err) + } + if err = db.DB().Where("repository_id = ? AND revision_id = ?", created.ID, activeRevisionID). + Delete(&po.RepositoryPackage{}).Error; err != nil { + t.Fatalf("delete active package: %v", err) + } + if err = service.Delete(ctx, created.ID, true); err != nil { + t.Fatalf("Delete() confirmed empty published repository error = %v", err) + } + if _, err = service.Describe(ctx, created.ID); !errors.Is(err, repositoryrepo.ErrRepositoryNotFound) { + t.Fatalf("Describe() deleted repository error = %v", err) + } +} + +func integrationContext(login string, global bool) entity.Context { + ctx := entity.NewDefaultContext() + ctx.User = &ftpb.UserInfo{LoginName: login} + if global { + ctx.User.Roles = []*ftpb.ModuleRole{{RoleId: request.DefaultGlobalRoleName}} + } + return ctx +} + +func integrationSpec(name string) request.SoftwareRepositorySpec { + return request.SoftwareRepositorySpec{ + ProjectID: integrationProjectID, + SystemProduct: "OpenCloudOS", + ProductVersion: "9.6", + Name: name, + Description: "Phase 1 MySQL integration repository", + Architecture: request.RepositoryArchitectureX8664, + Type: request.RepositoryTypePrivate, + AllowUpload: true, + IncludePackages: []string{"ocm-*"}, + ExcludePackages: []string{"*-debug"}, + GPGPolicy: request.RepositoryGPGVerifyPackage, + Enabled: true, + PrimaryStorageNodeID: integrationStorageNodeID, + } +} diff --git a/manager/backend/services/footstone/domain/repositoryService/service_test.go b/manager/backend/services/footstone/domain/repositoryService/service_test.go new file mode 100644 index 0000000000000000000000000000000000000000..440450a26dd061cf3ebe732a4dd07075eb14b37f --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryService/service_test.go @@ -0,0 +1,323 @@ +package repositoryService + +import ( + "database/sql" + "errors" + "strings" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + ftpb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/footstone" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositoryrepo" +) + +const testStorageNodeID = "22222222-2222-4222-8222-222222222222" + +func validSpec() request.SoftwareRepositorySpec { + return request.SoftwareRepositorySpec{ + ProjectID: 42, + SystemProduct: " OpenCloudOS ", + ProductVersion: " 9.6 ", + Name: "private-apps", + Description: " private packages ", + Architecture: "arm64", + Type: "private", + AllowUpload: true, + IncludePackages: []string{"app-*", "app-*"}, + GPGPolicy: "", + Enabled: true, + PrimaryStorageNodeID: testStorageNodeID, + } +} + +func TestNormalizeSpec(t *testing.T) { + got, err := NormalizeSpec(validSpec()) + if err != nil { + t.Fatalf("NormalizeSpec() error = %v", err) + } + if got.SystemProduct != "OpenCloudOS" || got.ProductVersion != "9.6" { + t.Fatalf("NormalizeSpec() labels = %q/%q", got.SystemProduct, got.ProductVersion) + } + if got.Architecture != request.RepositoryArchitectureARM64 { + t.Fatalf("NormalizeSpec() architecture = %q", got.Architecture) + } + if got.GPGPolicy != request.RepositoryGPGDisabled { + t.Fatalf("NormalizeSpec() GPG policy = %q", got.GPGPolicy) + } + if len(got.IncludePackages) != 1 { + t.Fatalf("NormalizeSpec() include = %v, want deduplicated", got.IncludePackages) + } +} + +func TestNormalizeSpecRejectsUnsafeValues(t *testing.T) { + tests := []struct { + name string + mutate func(*request.SoftwareRepositorySpec) + }{ + {name: "path in product", mutate: func(s *request.SoftwareRepositorySpec) { s.SystemProduct = "../etc" }}, + {name: "invalid name", mutate: func(s *request.SoftwareRepositorySpec) { s.Name = "private repo" }}, + {name: "upload to baseline", mutate: func(s *request.SoftwareRepositorySpec) { s.Type = "BASELINE" }}, + {name: "URL credentials", mutate: func(s *request.SoftwareRepositorySpec) { s.UpstreamURL = "https://user:pass@example.invalid/repo" }}, + {name: "unsupported arch", mutate: func(s *request.SoftwareRepositorySpec) { s.Architecture = "s390x" }}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + spec := validSpec() + test.mutate(&spec) + if _, err := NormalizeSpec(spec); err == nil { + t.Fatal("NormalizeSpec() error = nil") + } + }) + } +} + +func TestListRejectsUnsupportedFilters(t *testing.T) { + service := newService(nil, &fakeRepository{}, fakeStorageNodeReader{}, fakeProjectReader{}) + for _, filter := range []request.ListSoftwareRepositoriesRequest{ + {ProjectID: 42, Architecture: "s390x"}, + {ProjectID: 42, Type: "MIRROR"}, + } { + if _, _, err := service.List(globalContext(), filter); err == nil { + t.Fatalf("List(%#v) error = nil", filter) + } + } +} + +func TestNewSoftwareRepositoryUsesServerGeneratedRelativeStorage(t *testing.T) { + spec, err := NormalizeSpec(validSpec()) + if err != nil { + t.Fatal(err) + } + ctx := entity.NewDefaultContext() + ctx.User.LoginName = "admin" + value, err := newSoftwareRepository(ctx, spec, fakeStorageNode()) + if err != nil { + t.Fatal(err) + } + if strings.HasPrefix(value.StorageLocation, "/") || value.StorageLocation != "public/"+value.PublishKey { + t.Fatalf("StorageLocation = %q, want server-generated relative path", value.StorageLocation) + } + if value.Creator != "admin" || value.Status != request.RepositoryStatusEmpty { + t.Fatalf("repository creator/status = %q/%q", value.Creator, value.Status) + } +} + +type testSessionFactory struct{ db *gorm.DB } + +func (f testSessionFactory) NewSession() entity.Session { return f.db.Begin() } +func (f testSessionFactory) DB() entity.Session { return f.db } + +type fakeRepository struct { + createCount int + failAt int + value *po.SoftwareRepository +} + +func (f *fakeRepository) SyncTables() error { return nil } +func (f *fakeRepository) Create(_ *po.SoftwareRepository, _ entity.Session) error { + f.createCount++ + if f.failAt == f.createCount { + return repositoryrepo.ErrRepositoryDuplicate + } + return nil +} +func (f *fakeRepository) Update(*po.SoftwareRepository, entity.Session) error { return nil } +func (f *fakeRepository) GetByID(string, entity.Session) (*po.SoftwareRepository, error) { + if f.value != nil { + return f.value, nil + } + return nil, repositoryrepo.ErrRepositoryNotFound +} +func (f *fakeRepository) List(request.ListSoftwareRepositoriesRequest, entity.Session) ([]po.SoftwareRepository, request.PageResponse, error) { + return nil, request.PageResponse{}, nil +} +func (f *fakeRepository) Delete(string, entity.Session) error { return nil } +func (f *fakeRepository) UpdateStatus(string, string, entity.Session) error { return nil } +func (f *fakeRepository) UpdateSystemPurpose(string, bool, string, entity.Session) error { + return nil +} +func (f *fakeRepository) ActivateRevision(string, string, string, entity.Session) error { + return nil +} + +type fakeProjectReader struct{} + +type fakeStorageNodeReader struct{} + +func (fakeStorageNodeReader) GetNodeByID(id string, _ entity.Session) (*po.RepositoryStorageNode, error) { + if id != testStorageNodeID { + return nil, errors.New("storage node not found") + } + return fakeStorageNode(), nil +} + +func fakeStorageNode() *po.RepositoryStorageNode { + return &po.RepositoryStorageNode{ID: testStorageNodeID, ProjectID: 42, Enabled: true, + Status: request.RepositoryStorageNodeStatusOnline, PublishBaseURL: "http://repository-node:18090"} +} + +func (fakeProjectReader) DescribeProject(_ entity.Context, req request.DescribeProjectRequest, _ entity.Session) ( + request.DescribeProjectResponse, error, +) { + return request.DescribeProjectResponse{Project: po.Project{Base: po.Base{ID: req.ID}, Creator: "admin"}}, nil +} + +func newBatchTestService(t *testing.T, repo repositoryrepo.Repository) (Service, sqlmock.Sqlmock, *sql.DB) { + t.Helper() + sqlDB, mock, err := sqlmock.New() + if err != nil { + t.Fatal(err) + } + gormDB, err := gorm.Open(mysql.New(mysql.Config{Conn: sqlDB, SkipInitializeWithVersion: true}), + &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) + if err != nil { + t.Fatal(err) + } + return newService(testSessionFactory{db: gormDB}, repo, fakeStorageNodeReader{}, fakeProjectReader{}), mock, sqlDB +} + +func globalContext() entity.Context { + ctx := entity.NewDefaultContext() + ctx.User = &ftpb.UserInfo{LoginName: "admin", Roles: []*ftpb.ModuleRole{{RoleId: request.DefaultGlobalRoleName}}} + return ctx +} + +func TestBatchApplyRollsBackEveryItemOnFailure(t *testing.T) { + repo := &fakeRepository{failAt: 2} + service, mock, sqlDB := newBatchTestService(t, repo) + defer sqlDB.Close() + mock.ExpectBegin() + mock.ExpectRollback() + items := []request.BatchApplySoftwareRepositoryItem{{Spec: validSpec()}, {Spec: validSpec()}} + results, err := service.BatchApply(globalContext(), items) + if !errors.Is(err, repositoryrepo.ErrRepositoryDuplicate) { + t.Fatalf("BatchApply() error = %v", err) + } + if results != nil { + t.Fatalf("BatchApply() results = %v, want nil on atomic rollback", results) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } +} + +func TestBatchApplyCommitsAllItems(t *testing.T) { + repo := &fakeRepository{} + service, mock, sqlDB := newBatchTestService(t, repo) + defer sqlDB.Close() + mock.ExpectBegin() + mock.ExpectCommit() + items := []request.BatchApplySoftwareRepositoryItem{{Spec: validSpec()}, {Spec: validSpec()}} + results, err := service.BatchApply(globalContext(), items) + if err != nil { + t.Fatalf("BatchApply() error = %v", err) + } + if len(results) != 2 || repo.createCount != 2 { + t.Fatalf("BatchApply() result/create count = %d/%d", len(results), repo.createCount) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } +} + +func TestBatchApplyInSessionLeavesTransactionOwnershipToCaller(t *testing.T) { + repo := &fakeRepository{} + service, mock, sqlDB := newBatchTestService(t, repo) + defer sqlDB.Close() + mock.ExpectBegin() + mock.ExpectRollback() + + tx := service.db.NewSession() + results, err := service.BatchApplyInSession(globalContext(), + []request.BatchApplySoftwareRepositoryItem{{Spec: validSpec()}}, tx) + if err != nil { + t.Fatalf("BatchApplyInSession() error = %v", err) + } + if len(results) != 1 || repo.createCount != 1 { + t.Fatalf("BatchApplyInSession() result/create count = %d/%d", len(results), repo.createCount) + } + if err = tx.Rollback().Error; err != nil { + t.Fatalf("rollback caller-owned transaction: %v", err) + } + if err = mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } +} + +func TestDeleteAllowsHistoricalRevisionAndCleansQuarantine(t *testing.T) { + repositoryID := "11111111-1111-4111-8111-111111111111" + repo := &fakeRepository{value: &po.SoftwareRepository{ + ID: repositoryID, ProjectID: 42, Creator: "admin", + }} + service, mock, sqlDB := newBatchTestService(t, repo) + defer sqlDB.Close() + mock.ExpectBegin() + mock.ExpectQuery("SELECT count\\(\\*\\) FROM `repository_uploads` WHERE repository_id = \\? AND status IN"). + WithArgs(repositoryID, sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg()). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0)) + mock.ExpectQuery("SELECT count\\(\\*\\) FROM `repository_gpg_keys` WHERE repository_id = \\?"). + WithArgs(repositoryID).WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0)) + mock.ExpectQuery("SELECT count\\(\\*\\) FROM `repository_tasks` WHERE repository_id = \\? AND status IN"). + WithArgs(repositoryID, sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg()). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0)) + mock.ExpectExec("DELETE FROM `repository_quarantine_items` WHERE repository_id = \\?"). + WithArgs(repositoryID).WillReturnResult(sqlmock.NewResult(0, 1)) + mock.ExpectCommit() + + err := service.Delete(globalContext(), repositoryID, true) + if err != nil { + t.Fatalf("Delete() error = %v", err) + } + if err = mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } +} + +func TestDeleteRejectsActiveRevisionPackages(t *testing.T) { + repositoryID := "11111111-1111-4111-8111-111111111111" + revisionID := "33333333-3333-4333-8333-333333333333" + repo := &fakeRepository{value: &po.SoftwareRepository{ + ID: repositoryID, ProjectID: 42, Creator: "admin", ActiveRevisionID: revisionID, + }} + service, mock, sqlDB := newBatchTestService(t, repo) + defer sqlDB.Close() + mock.ExpectBegin() + mock.ExpectQuery("SELECT count\\(\\*\\) FROM `repository_packages` WHERE repository_id = \\? AND revision_id = \\?"). + WithArgs(repositoryID, revisionID).WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) + mock.ExpectRollback() + + err := service.Delete(globalContext(), repositoryID, true) + if !errors.Is(err, ErrRepositoryHasContent) { + t.Fatalf("Delete() error = %v, want ErrRepositoryHasContent", err) + } + if err = mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } +} + +func TestDeleteRejectsSystemManagedRepository(t *testing.T) { + repositoryID := "11111111-1111-4111-8111-111111111111" + repo := &fakeRepository{value: &po.SoftwareRepository{ + ID: repositoryID, ProjectID: 42, Creator: "admin", SystemManaged: true, + Purpose: request.RepositoryPurposeNodeRPM, + }} + service, mock, sqlDB := newBatchTestService(t, repo) + defer sqlDB.Close() + mock.ExpectBegin() + mock.ExpectRollback() + + err := service.Delete(globalContext(), repositoryID, true) + if !errors.Is(err, ErrSystemManagedRepository) { + t.Fatalf("Delete() error = %v, want ErrSystemManagedRepository", err) + } + if err = mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } +} diff --git a/manager/backend/services/footstone/domain/repositoryTaskService/recovery_context_test.go b/manager/backend/services/footstone/domain/repositoryTaskService/recovery_context_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d906af6ee9c7c3fb636b7f76074400a6070c98b8 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryTaskService/recovery_context_test.go @@ -0,0 +1,79 @@ +package repositoryTaskService + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorytaskrepo" +) + +type recoveryContextFactory struct{ db *gorm.DB } + +func (f recoveryContextFactory) DB() entity.Session { return f.db } +func (f recoveryContextFactory) NewSession() entity.Session { return f.db.Begin() } + +type recoveryContextTasks struct { + repositorytaskrepo.Repository + find func(entity.Session) ([]po.RepositoryTask, error) +} + +func (f recoveryContextTasks) FindExpired(_ time.Time, _ int, session entity.Session) ([]po.RepositoryTask, error) { + return f.find(session) +} + +func TestRecoverExpiredContextPropagatesCancellationToTransaction(t *testing.T) { + sqlDB, mock, err := sqlmock.New() + if err != nil { + t.Fatal(err) + } + defer sqlDB.Close() + db, err := gorm.Open(mysql.New(mysql.Config{Conn: sqlDB, SkipInitializeWithVersion: true}), + &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) + if err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + mock.ExpectBegin() + mock.ExpectRollback() + service := &service{db: recoveryContextFactory{db}, now: time.Now, + tasks: recoveryContextTasks{find: func(session entity.Session) ([]po.RepositoryTask, error) { + if session.Statement.Context != ctx { + t.Fatal("recovery transaction lost lifecycle context") + } + cancel() + return nil, session.Statement.Context.Err() + }}} + if count, err := service.RecoverExpiredContext(ctx); count != 0 || !errors.Is(err, context.Canceled) { + t.Fatalf("canceled recovery count/error = %d/%v", count, err) + } + // database/sql may perform the cancellation rollback asynchronously. + deadline := time.Now().Add(time.Second) + for { + if err = mock.ExpectationsWereMet(); err == nil { + break + } + if time.Now().After(deadline) { + t.Fatal(err) + } + time.Sleep(time.Millisecond) + } +} + +func TestRecoverExpiredContextDoesNotOpenTransactionAfterShutdown(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + service := &service{} + if count, err := service.RecoverExpiredContext(ctx); count != 0 || !errors.Is(err, context.Canceled) { + t.Fatalf("shutdown recovery count/error = %d/%v", count, err) + } +} diff --git a/manager/backend/services/footstone/domain/repositoryTaskService/service.go b/manager/backend/services/footstone/domain/repositoryTaskService/service.go new file mode 100644 index 0000000000000000000000000000000000000000..f8faa76c514f6c82484698957e0f21197f838706 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryTaskService/service.go @@ -0,0 +1,1663 @@ +package repositoryTaskService + +import ( + "context" + "crypto/sha256" + "encoding/json" + "errors" + "fmt" + "regexp" + "strconv" + "strings" + "time" + "unicode" + "unicode/utf8" + + "github.com/google/uuid" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/infrarepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorylockrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositoryrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorytaskrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorytaskresultrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryDiagnosisService" +) + +const ( + defaultMaxAttempts = 3 + maxTaskPayloadBytes = 1 << 20 + claimCandidateLimit = 32 + recoveryBatchLimit = 100 + minLeaseDuration = 5 * time.Second + maxLeaseDuration = 5 * time.Minute +) + +var ( + ErrTaskNotRunnable = errors.New("repository task is not runnable") + ErrTaskNotCancelable = errors.New("repository task cannot be canceled") + ErrTaskNotRetryable = errors.New("repository task is not retryable") + ErrTaskOwnershipLost = errors.New("repository task ownership is lost") + ErrTaskLeaseExpired = errors.New("repository task lease is expired") + ErrInvalidTaskTransition = errors.New("invalid repository task status transition") + ErrDiagnosisNotAvailable = errors.New("repository task diagnosis is not available") + ErrRepositoryMigrationRequired = errors.New("repository must be assigned to a storage node before tasks can run") + ErrTaskCommitMismatch = errors.New("repository task commit does not match the prepared result") + ErrTaskStorageNodeMismatch = errors.New("repository task is assigned to another storage node") + workerIDPattern = regexp.MustCompile(`^[A-Za-z0-9._:-]+$`) + sha256HexPattern = regexp.MustCompile(`^[0-9a-f]{64}$`) +) + +type projectReader interface { + DescribeProject(entity.Context, request.DescribeProjectRequest, entity.Session) (request.DescribeProjectResponse, error) +} + +type taskPreflightGuard interface { + PrepareTaskPreflight(entity.Context, *po.SoftwareRepository, string, string, + request.EnqueueRepositoryTaskRequest, entity.Session) (*po.RepositoryPreflightRecord, error) + RevalidateTaskPreflight(*po.RepositoryTask, entity.Session) error +} + +type Service = *service + +type service struct { + db entity.SessionFactory + repositories repositoryrepo.Repository + tasks repositorytaskrepo.Repository + targets repositorytaskrepo.TargetRepository + results repositorytaskresultrepo.Repository + locks repositorylockrepo.Repository + projects projectReader + preflight taskPreflightGuard + now func() time.Time +} + +func New(db *database.DBClient) Service { + value := newService(db, repositoryrepo.New(db), repositorytaskrepo.New(db), repositorylockrepo.New(db), + infrarepo.NewInfrastructureRepository(db)) + value.targets = repositorytaskrepo.NewTargetRepository(db) + value.results = repositorytaskresultrepo.New(db) + return value +} + +func newService(db entity.SessionFactory, repositories repositoryrepo.Repository, tasks repositorytaskrepo.Repository, + locks repositorylockrepo.Repository, projects projectReader, +) Service { + return &service{ + db: db, + repositories: repositories, + tasks: tasks, + locks: locks, + projects: projects, + now: func() time.Time { return time.Now().UTC() }, + } +} + +func (s *service) SyncTables() error { + if err := s.tasks.SyncTables(); err != nil { + return err + } + if s.targets != nil { + if err := s.targets.SyncTables(); err != nil { + return err + } + } + return s.locks.SyncTables() +} + +func (s *service) ConfigurePreflight(guard taskPreflightGuard) { + s.preflight = guard +} + +func (s *service) Enqueue(ctx entity.Context, input request.EnqueueRepositoryTaskRequest) ( + value *po.RepositoryTask, err error, +) { + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + return s.EnqueueInSession(ctx, input, tx) +} + +// EnqueueInSession lets product actions atomically create their domain record +// and durable task without exposing a generic enqueue management API. +func (s *service) EnqueueInSession(ctx entity.Context, input request.EnqueueRepositoryTaskRequest, + session entity.Session, +) (value *po.RepositoryTask, err error) { + return s.enqueueInSession(ctx, input, "", session) +} + +// EnqueuePackageInstallInSession creates one logical PACKAGE_INSTALL task and +// its per-Agent targets. The task remains in the existing task model but is +// explicitly routed to TMS, so repository workers will never claim it. +func (s *service) EnqueuePackageInstallInSession(ctx entity.Context, input request.EnqueueRepositoryTaskRequest, + targetAgentIDs []uint64, session entity.Session, +) (value *po.RepositoryTask, err error) { + if strings.ToUpper(strings.TrimSpace(input.Type)) != request.RepositoryTaskTypePackageInstall { + return nil, fmt.Errorf("targeted TMS enqueue is only valid for PACKAGE_INSTALL tasks") + } + if len(targetAgentIDs) == 0 || len(targetAgentIDs) > 100 { + return nil, fmt.Errorf("target_agent_ids must contain between 1 and 100 entries") + } + value, err = s.enqueueInSession(ctx, input, "", session) + if err != nil { + return nil, err + } + value.ExecutorKind = request.RepositoryTaskExecutorTMS + value.CurrentStep = "waiting for package installation dispatch" + if err = s.tasks.Update(value, session); err != nil { + return nil, err + } + seen := make(map[uint64]struct{}, len(targetAgentIDs)) + for _, agentID := range targetAgentIDs { + if agentID == 0 { + return nil, fmt.Errorf("target_agent_ids cannot contain zero") + } + if _, ok := seen[agentID]; ok { + continue + } + seen[agentID] = struct{}{} + targetID := strconv.FormatUint(agentID, 10) + if err = s.targets.CreateTarget(&po.RepositoryTaskTarget{ + ID: uuid.NewString(), TaskID: value.ID, TargetType: "TMS_AGENT", TargetID: targetID, + TMSAgentID: agentID, Status: request.RepositoryTaskStatusPending, + }, session); err != nil { + return nil, err + } + } + value.Targets, err = s.targets.ListTargets(value.ID, session) + return value, err +} + +// RecordInventoryImportInSession records the terminal control-plane operation +// in the shared repository task stream. Inventory import remains a synchronous +// database transaction, so it never enters the repository-worker claim queue. +func (s *service) RecordInventoryImportInSession(ctx entity.Context, projectID int64, previewID, + payloadJSON string, session entity.Session, +) (*po.RepositoryTask, error) { + if projectID <= 0 { + return nil, errors.New("project_id must be greater than zero") + } + if err := validateUUID("preview_id", previewID); err != nil { + return nil, err + } + if ctx.User == nil || strings.TrimSpace(ctx.User.LoginName) == "" { + return nil, errors.New("authenticated user is required") + } + if err := s.authorizeProject(ctx, projectID, session); err != nil { + return nil, err + } + payloadJSON = strings.TrimSpace(payloadJSON) + if payloadJSON == "" { + payloadJSON = "{}" + } + if len(payloadJSON) > maxTaskPayloadBytes || !json.Valid([]byte(payloadJSON)) { + return nil, fmt.Errorf("task payload must be valid JSON and no larger than %d bytes", maxTaskPayloadBytes) + } + idempotencyKey := "inventory-import:" + previewID + if existing, err := s.tasks.GetByIdempotencyKey(idempotencyKey, session); err == nil { + return existing, nil + } else if !errors.Is(err, repositorytaskrepo.ErrTaskNotFound) { + return nil, err + } + now := s.now() + id := uuid.NewString() + value := &po.RepositoryTask{ + ID: id, RootTaskID: id, ProjectID: projectID, + ScopeType: request.RepositoryTaskScopeProject, ScopeID: strconv.FormatInt(projectID, 10), + ExecutorKind: request.RepositoryTaskExecutorManual, + Type: request.RepositoryTaskTypeInventoryImport, Trigger: request.RepositoryTaskTriggerManual, + IdempotencyKey: optionalString(idempotencyKey), Status: request.RepositoryTaskStatusSuccess, + RequiresMutationLock: false, MaxAttempts: 1, Attempt: 1, + RequestedBy: ctx.User.LoginName, AvailableAt: now, StartedAt: timePointer(now), FinishedAt: timePointer(now), + ProgressPercent: 100, CurrentStep: "inventory import committed", PayloadJSON: payloadJSON, + CommitState: request.RepositoryTaskCommitStateNone, + } + if err := s.tasks.Create(value, session); err != nil { + if errors.Is(err, repositorytaskrepo.ErrTaskDuplicate) { + return s.tasks.GetByIdempotencyKey(idempotencyKey, session) + } + return nil, err + } + return value, nil +} + +// EnqueueMigrationInSession is the only task entry point allowed to override a repository's node assignment. +// It lets a MIGRATION_REQUIRED repository target a registered node without weakening normal task routing. +func (s *service) EnqueueMigrationInSession(ctx entity.Context, input request.EnqueueRepositoryTaskRequest, + targetStorageNodeID string, session entity.Session, +) (value *po.RepositoryTask, err error) { + if strings.ToUpper(strings.TrimSpace(input.Type)) != request.RepositoryTaskTypeMigrateRepository { + return nil, fmt.Errorf("storage node override is only valid for MIGRATE_REPOSITORY tasks") + } + if err = validateUUID("target_storage_node_id", targetStorageNodeID); err != nil { + return nil, err + } + return s.enqueueInSession(ctx, input, strings.TrimSpace(targetStorageNodeID), session) +} + +func (s *service) enqueueInSession(ctx entity.Context, input request.EnqueueRepositoryTaskRequest, + targetStorageNodeID string, session entity.Session, +) (value *po.RepositoryTask, err error) { + input, err = normalizeEnqueueRequest(input) + if err != nil { + return nil, err + } + repository, err := s.authorizeRepository(ctx, input.RepositoryID, session) + if err != nil { + return nil, err + } + if repository.Status == request.RepositoryStatusMigrationRequired && targetStorageNodeID == "" { + return nil, ErrRepositoryMigrationRequired + } + now := s.now() + if input.IdempotencyKey != "" { + if existing, lookupErr := s.tasks.GetByIdempotencyKey(input.IdempotencyKey, session); lookupErr == nil { + return existing, nil + } else if !errors.Is(lookupErr, repositorytaskrepo.ErrTaskNotFound) { + return nil, lookupErr + } + } + id := uuid.NewString() + storageNodeID := repository.PrimaryStorageNodeID + if targetStorageNodeID != "" { + storageNodeID = targetStorageNodeID + } + value = &po.RepositoryTask{ + ID: id, + RootTaskID: id, + RepositoryID: repository.ID, + StorageNodeID: storageNodeID, + PreflightID: input.PreflightID, + ProjectID: repository.ProjectID, + ScopeType: request.RepositoryTaskScopeRepository, + ScopeID: repository.ID, + ExecutorKind: request.RepositoryTaskExecutorWorker, + RevisionID: input.RevisionID, + Type: input.Type, + Trigger: input.Trigger, + IdempotencyKey: optionalString(input.IdempotencyKey), + BandwidthLimitBytes: input.BandwidthLimitBytes, + MaxParallelDownloads: input.MaxParallelDownloads, + Status: request.RepositoryTaskStatusPending, + RequiresMutationLock: requiresMutationLock(input.Type), + MaxAttempts: input.MaxAttempts, + RequestedBy: ctx.User.LoginName, + AvailableAt: now, + PayloadJSON: input.PayloadJSON, + CommitState: "NONE", + } + if value.StorageNodeID == "" { + return nil, ErrRepositoryMigrationRequired + } + if value.RequiresMutationLock && s.preflight != nil { + record, preflightErr := s.preflight.PrepareTaskPreflight(ctx, repository, value.ID, + value.StorageNodeID, input, session) + if preflightErr != nil { + return nil, preflightErr + } + value.PreflightID = record.ID + if record.Status != request.RepositoryPreflightStatusPass { + value.Status = request.RepositoryTaskStatusFailed + value.ErrorCode, value.ErrorMessage = record.ErrorCode, record.ErrorMessage + value.FinishedAt = timePointer(now) + diagnosis := repositoryDiagnosisService.Analyze(repositoryDiagnosisService.Context{ + TaskType: value.Type, Status: value.Status, ErrorCode: value.ErrorCode, + ErrorMessage: value.ErrorMessage, CurrentStep: "repository node preflight", + }, now) + if err = repositoryDiagnosisService.Apply(value, diagnosis); err != nil { + return nil, err + } + } + } + if err = s.tasks.Create(value, session); err != nil { + if errors.Is(err, repositorytaskrepo.ErrTaskDuplicate) && input.IdempotencyKey != "" { + return s.tasks.GetByIdempotencyKey(input.IdempotencyKey, session) + } + return nil, err + } + return value, nil +} + +func (s *service) Describe(ctx entity.Context, taskID string) (*po.RepositoryTask, error) { + if err := validateUUID("task_id", taskID); err != nil { + return nil, err + } + value, err := s.tasks.GetByID(taskID, nil) + if err != nil { + return nil, err + } + if value.RepositoryID != "" { + if _, err = s.authorizeRepository(ctx, value.RepositoryID, nil); err != nil { + return nil, err + } + } else if err = s.authorizeProject(ctx, value.ProjectID, nil); err != nil { + return nil, err + } + if s.targets != nil { + value.Targets, err = s.targets.ListTargets(value.ID, nil) + if err != nil { + return nil, err + } + } + if s.results != nil { + value.ResultItems, err = s.results.ListByTaskID(value.ID, nil) + if err != nil { + return nil, err + } + } + return value, nil +} + +// RecordPackageDeleteResults records one immutable audit row per selected +// package. It is called inside the same transaction that publishes the single +// replacement revision, so a successful item can never outlive a failed +// active-revision switch. +func (s *service) RecordPackageDeleteResults(task *po.RepositoryTask, nevras []string, + session entity.Session, +) error { + return s.recordPackageDeleteResults(task, nevras, request.RepositoryTaskStatusSuccess, "", + "package deleted from published revision", session) +} + +func (s *service) recordPackageDeleteResults(task *po.RepositoryTask, nevras []string, status, errorCode, + message string, session entity.Session, +) error { + if task == nil || task.Type != request.RepositoryTaskTypeDeletePackage || session == nil { + return errors.New("delete package task and transaction are required") + } + if status != request.RepositoryTaskStatusSuccess && status != request.RepositoryTaskStatusFailed && + status != request.RepositoryTaskStatusCanceled { + return errors.New("delete package result status is invalid") + } + if uuid.Validate(task.ID) != nil || len(nevras) == 0 || len(nevras) > 100 { + return errors.New("delete package result identity is invalid") + } + now := s.now() + seen := make(map[string]struct{}, len(nevras)) + items := make([]po.RepositoryTaskResultItem, 0, len(nevras)) + for _, value := range nevras { + nevra := strings.TrimSpace(value) + if nevra == "" || len(nevra) > 512 { + return errors.New("delete package result contains an invalid NEVRA") + } + if _, exists := seen[nevra]; exists { + continue + } + seen[nevra] = struct{}{} + resultJSON, _ := json.Marshal(map[string]bool{"deleted": status == request.RepositoryTaskStatusSuccess}) + items = append(items, po.RepositoryTaskResultItem{ + ID: uuid.NewSHA1(uuid.MustParse(task.ID), []byte(nevra)).String(), + TaskID: task.ID, ItemKey: nevra, PackageNEVRA: nevra, + Status: status, ErrorCode: errorCode, Message: message, + ResultJSON: resultJSON, StartedAt: task.StartedAt, FinishedAt: &now, + }) + } + return s.results.CreateItems(items, session) +} + +func deletePackageTaskNEVRAs(payloadJSON string) ([]string, error) { + var payload struct { + NEVRA string `json:"nevra"` + NEVRAs []string `json:"nevras"` + } + if err := json.Unmarshal([]byte(payloadJSON), &payload); err != nil { + return nil, err + } + if len(payload.NEVRAs) == 0 && strings.TrimSpace(payload.NEVRA) != "" { + payload.NEVRAs = []string{payload.NEVRA} + } + return payload.NEVRAs, nil +} + +// DescribeInternal is reserved for authenticated repository-node operations. +func (s *service) DescribeInternal(taskID string) (*po.RepositoryTask, error) { + if err := validateUUID("task_id", taskID); err != nil { + return nil, err + } + return s.tasks.GetByID(taskID, nil) +} + +func (s *service) HasUnfinishedMutation(repositoryID string, session entity.Session) (bool, error) { + if err := validateUUID("repository_id", repositoryID); err != nil { + return false, err + } + return s.tasks.HasUnfinishedMutation(repositoryID, session) +} + +func (s *service) HasUnfinishedType(repositoryID, taskType string, session entity.Session) (bool, error) { + if err := validateUUID("repository_id", repositoryID); err != nil { + return false, err + } + taskType = strings.ToUpper(strings.TrimSpace(taskType)) + if !isTaskType(taskType) { + return false, fmt.Errorf("unsupported repository task type %q", taskType) + } + return s.tasks.HasUnfinishedType(repositoryID, taskType, session) +} + +func (s *service) GetByIdempotencyKey(key string, session entity.Session) (*po.RepositoryTask, error) { + key = strings.TrimSpace(key) + if key == "" || len(key) > 128 || containsControl(key) { + return nil, fmt.Errorf("idempotency_key is invalid or exceeds 128 characters") + } + return s.tasks.GetByIdempotencyKey(key, session) +} + +func (s *service) ExportDiagnosis(ctx entity.Context, taskID string) (*request.RepositoryDiagnosisExport, error) { + value, err := s.Describe(ctx, taskID) + if err != nil { + return nil, err + } + diagnosis := value.StructuredDiagnosis() + if diagnosis == nil { + return nil, ErrDiagnosisNotAvailable + } + report := struct { + SchemaVersion string `json:"schema_version"` + TaskID string `json:"task_id"` + RootTaskID string `json:"root_task_id"` + RepositoryID string `json:"repository_id"` + RevisionID string `json:"revision_id,omitempty"` + TaskType string `json:"task_type"` + TaskStatus string `json:"task_status"` + Attempt int `json:"attempt"` + CurrentStep string `json:"current_step,omitempty"` + Diagnosis *po.RepositoryDiagnosis `json:"diagnosis"` + }{ + SchemaVersion: "ocmanager.repository.diagnosis/v1", TaskID: value.ID, + RootTaskID: value.RootTaskID, RepositoryID: value.RepositoryID, RevisionID: value.RevisionID, + TaskType: value.Type, TaskStatus: value.Status, Attempt: value.Attempt, + CurrentStep: repositoryDiagnosisService.Sanitize(value.CurrentStep), Diagnosis: diagnosis, + } + content, err := json.MarshalIndent(report, "", " ") + if err != nil { + return nil, fmt.Errorf("encode repository diagnosis report: %w", err) + } + content = append(content, '\n') + digest := sha256.Sum256(content) + return &request.RepositoryDiagnosisExport{ + FileName: fmt.Sprintf("repository-diagnosis-%s.json", value.ID), + ContentType: "application/json", SHA256: fmt.Sprintf("%x", digest), Content: string(content), + }, nil +} + +func (s *service) List(ctx entity.Context, filter request.ListRepositoryTasksRequest) ( + []po.RepositoryTask, request.PageResponse, error, +) { + values, page, _, err := s.ListWithStatistics(ctx, filter) + return values, page, err +} + +func (s *service) ListWithStatistics(ctx entity.Context, filter request.ListRepositoryTasksRequest) ( + []po.RepositoryTask, request.PageResponse, request.RepositoryTaskStatistics, error, +) { + var statistics request.RepositoryTaskStatistics + if filter.RepositoryID != "" { + if err := validateUUID("repository_id", filter.RepositoryID); err != nil { + return nil, request.PageResponse{}, statistics, err + } + repository, err := s.authorizeRepository(ctx, filter.RepositoryID, nil) + if err != nil { + return nil, request.PageResponse{}, statistics, err + } + if filter.ProjectID != 0 && filter.ProjectID != repository.ProjectID { + return nil, request.PageResponse{}, statistics, + errors.New("repository does not belong to the requested project") + } + filter.ProjectID = repository.ProjectID + } else { + if filter.ProjectID <= 0 { + return nil, request.PageResponse{}, statistics, errors.New("project_id must be greater than zero") + } + if err := s.authorizeProject(ctx, filter.ProjectID, nil); err != nil { + return nil, request.PageResponse{}, statistics, err + } + } + if filter.StorageNodeID != "" { + if err := validateUUID("storage_node_id", filter.StorageNodeID); err != nil { + return nil, request.PageResponse{}, statistics, err + } + } + filter.Type = strings.ToUpper(strings.TrimSpace(filter.Type)) + if filter.Type != "" && !isTaskType(filter.Type) { + return nil, request.PageResponse{}, statistics, + fmt.Errorf("unsupported repository task type %q", filter.Type) + } + filter.Status = strings.ToUpper(strings.TrimSpace(filter.Status)) + if filter.Status != "" && !isTaskStatus(filter.Status) { + return nil, request.PageResponse{}, statistics, + fmt.Errorf("unsupported repository task status %q", filter.Status) + } + normalizedTrigger, err := normalizeTaskTrigger(filter.Trigger, false) + if err != nil { + return nil, request.PageResponse{}, statistics, + err + } + filter.Trigger = normalizedTrigger + if filter.CreatedAtFrom != nil { + value := filter.CreatedAtFrom.UTC() + filter.CreatedAtFrom = &value + } + if filter.CreatedAtTo != nil { + value := filter.CreatedAtTo.UTC() + filter.CreatedAtTo = &value + } + if filter.CreatedAtFrom != nil && filter.CreatedAtTo != nil && filter.CreatedAtFrom.After(*filter.CreatedAtTo) { + return nil, request.PageResponse{}, statistics, errors.New("created_at_from must not be after created_at_to") + } + if filter.PageRequest.PageNumber <= 0 { + filter.PageRequest.PageNumber = 1 + } + if filter.PageRequest.PageSize <= 0 { + filter.PageRequest.PageSize = 50 + } + if filter.PageRequest.PageSize > 200 { + return nil, request.PageResponse{}, statistics, fmt.Errorf("page_size cannot exceed 200") + } + values, page, err := s.tasks.List(filter, nil) + if err != nil { + return nil, request.PageResponse{}, statistics, err + } + statisticsRepository, ok := s.tasks.(repositorytaskrepo.StatisticsRepository) + if !ok { + return nil, request.PageResponse{}, statistics, + errors.New("repository task statistics are unavailable") + } + statistics, err = statisticsRepository.Statistics(filter, nil) + if err != nil { + return nil, request.PageResponse{}, statistics, err + } + if s.targets == nil { + return values, page, statistics, nil + } + for index := range values { + values[index].Targets, err = s.targets.ListTargets(values[index].ID, nil) + if err != nil { + return nil, request.PageResponse{}, statistics, err + } + } + return values, page, statistics, nil +} + +func (s *service) authorizeProject(ctx entity.Context, projectID int64, session entity.Session) error { + if ctx.User == nil || strings.TrimSpace(ctx.User.LoginName) == "" { + return errors.New("authenticated user is required") + } + project, err := s.projects.DescribeProject(ctx, request.DescribeProjectRequest{ID: projectID}, session) + if err != nil { + return fmt.Errorf("project %d not found: %w", projectID, err) + } + for _, role := range ctx.User.Roles { + if role.RoleId == request.DefaultGlobalRoleName { + return nil + } + } + if project.Project.Creator != ctx.User.LoginName { + return errors.New("user cannot manage repository tasks in this project") + } + return nil +} + +func (s *service) Cancel(ctx entity.Context, taskID string) (value *po.RepositoryTask, err error) { + if err = validateUUID("task_id", taskID); err != nil { + return nil, err + } + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + value, err = s.tasks.GetByIDForUpdate(taskID, tx) + if err != nil { + return nil, err + } + if _, err = s.authorizeRepository(ctx, value.RepositoryID, tx); err != nil { + return nil, err + } + now := s.now() + switch value.Status { + case request.RepositoryTaskStatusPending: + value.Status = request.RepositoryTaskStatusCanceled + value.CancelRequestedAt = timePointer(now) + value.FinishedAt = timePointer(now) + case request.RepositoryTaskStatusRunning: + if value.RevisionID != "" && value.Type != request.RepositoryTaskTypePackageInstall { + return nil, ErrTaskNotCancelable + } + value.Status = request.RepositoryTaskStatusCancelRequested + value.CancelRequestedAt = timePointer(now) + case request.RepositoryTaskStatusCancelRequested, request.RepositoryTaskStatusCanceled, + request.RepositoryTaskStatusSuccess, request.RepositoryTaskStatusFailed, + request.RepositoryTaskStatusPartialSuccess: + return value, nil + default: + return nil, ErrTaskNotCancelable + } + if err = s.tasks.Update(value, tx); err != nil { + return nil, err + } + return value, nil +} + +func (s *service) Retry(ctx entity.Context, taskID string) (value *po.RepositoryTask, err error) { + if err = validateUUID("task_id", taskID); err != nil { + return nil, err + } + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + original, err := s.tasks.GetByIDForUpdate(taskID, tx) + if err != nil { + return nil, err + } + repository, err := s.authorizeRepository(ctx, original.RepositoryID, tx) + if err != nil { + return nil, err + } + if original.Status != request.RepositoryTaskStatusFailed && original.Status != request.RepositoryTaskStatusCanceled { + return nil, ErrTaskNotRetryable + } + if original.Type == request.RepositoryTaskTypeNodeInstall || original.Type == request.RepositoryTaskTypePackageInstall { + return nil, ErrTaskNotRetryable + } + retries, ok := s.tasks.(repositorytaskrepo.RetryRepository) + if !ok { + return nil, errors.New("repository retry lookup is unavailable") + } + // GetByIDForUpdate above serializes retry requests for this parent. A locking + // read also sees a child committed by a preceding request under repeatable read. + existing, lookupErr := retries.FindUnfinishedRetry(original, tx) + if lookupErr == nil { + if existing == nil || existing.RepositoryID != original.RepositoryID || + existing.ProjectID != original.ProjectID || existing.RetryOfTaskID != original.ID || + existing.Type != original.Type { + return nil, errors.New("repository retry task identity mismatch") + } + return existing, nil + } + if !errors.Is(lookupErr, repositorytaskrepo.ErrTaskNotFound) { + return nil, lookupErr + } + now := s.now() + id := uuid.NewString() + value = &po.RepositoryTask{ + ID: id, + RootTaskID: original.RootTaskID, + RetryOfTaskID: original.ID, + RepositoryID: original.RepositoryID, + StorageNodeID: original.StorageNodeID, + PreflightID: original.PreflightID, + ProjectID: original.ProjectID, + ScopeType: original.ScopeType, + ScopeID: original.ScopeID, + ExecutorKind: original.ExecutorKind, + RevisionID: original.RevisionID, + Type: original.Type, + Trigger: request.RepositoryTaskTriggerRetry, + BandwidthLimitBytes: original.BandwidthLimitBytes, + MaxParallelDownloads: original.MaxParallelDownloads, + Status: request.RepositoryTaskStatusPending, + RequiresMutationLock: original.RequiresMutationLock, + MaxAttempts: original.MaxAttempts, + RequestedBy: ctx.User.LoginName, + AvailableAt: now, + PayloadJSON: original.PayloadJSON, + CommitState: "NONE", + } + if value.RequiresMutationLock && s.preflight != nil { + record, preflightErr := s.preflight.PrepareTaskPreflight(ctx, repository, value.ID, value.StorageNodeID, + request.EnqueueRepositoryTaskRequest{RepositoryID: value.RepositoryID, RevisionID: value.RevisionID, + Type: value.Type, PayloadJSON: value.PayloadJSON, MaxAttempts: value.MaxAttempts}, tx) + if preflightErr != nil { + return nil, preflightErr + } + value.PreflightID = record.ID + if record.Status != request.RepositoryPreflightStatusPass { + value.Status = request.RepositoryTaskStatusFailed + value.ErrorCode, value.ErrorMessage = record.ErrorCode, record.ErrorMessage + value.FinishedAt = timePointer(now) + diagnosis := repositoryDiagnosisService.Analyze(repositoryDiagnosisService.Context{ + TaskType: value.Type, Status: value.Status, ErrorCode: value.ErrorCode, + ErrorMessage: value.ErrorMessage, CurrentStep: "repository node preflight", + }, now) + if err = repositoryDiagnosisService.Apply(value, diagnosis); err != nil { + return nil, err + } + } + } + if err = s.tasks.Create(value, tx); err != nil { + return nil, err + } + return value, nil +} + +func (s *service) ClaimNext(workerID string, lease time.Duration) (value *po.RepositoryTask, err error) { + return s.ClaimNextForNode("", workerID, lease) +} + +func (s *service) ClaimNextForNode(storageNodeID, workerID string, lease time.Duration) ( + value *po.RepositoryTask, err error, +) { + if err = validateWorkerAndLease(workerID, lease); err != nil { + return nil, err + } + if storageNodeID != "" { + if err = validateUUID("storage_node_id", storageNodeID); err != nil { + return nil, err + } + } + now := s.now() + leaseUntil := now.Add(lease) + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + excludedRepositories := make([]string, 0) + for attempt := 0; attempt < claimCandidateLimit; attempt++ { + candidate, findErr := s.tasks.FindClaimCandidate(now, storageNodeID, excludedRepositories, tx) + if findErr != nil { + return nil, findErr + } + if candidate == nil { + return nil, nil + } + if candidate.RequiresMutationLock && s.preflight != nil { + if preflightErr := s.preflight.RevalidateTaskPreflight(candidate, tx); preflightErr != nil { + candidate.Status = request.RepositoryTaskStatusFailed + candidate.ErrorCode, candidate.ErrorMessage = preflightErrorDetail(preflightErr) + candidate.CurrentStep = "repository node preflight" + candidate.FinishedAt = timePointer(now) + diagnosis := repositoryDiagnosisService.Analyze(repositoryDiagnosisService.Context{ + TaskType: candidate.Type, Status: candidate.Status, ErrorCode: candidate.ErrorCode, + ErrorMessage: candidate.ErrorMessage, CurrentStep: candidate.CurrentStep, + }, now) + if err = repositoryDiagnosisService.Apply(candidate, diagnosis); err != nil { + return nil, err + } + if err = s.tasks.Update(candidate, tx); err != nil { + return nil, err + } + continue + } + } + var fencingToken uint64 + if candidate.RequiresMutationLock { + fencingToken, err = s.locks.TryAcquire(candidate.RepositoryID, candidate.ID, workerID, now, leaseUntil, tx) + if errors.Is(err, repositorylockrepo.ErrLockHeld) { + err = nil + excludedRepositories = append(excludedRepositories, candidate.RepositoryID) + continue + } + if err != nil { + return nil, err + } + } + candidate.Status = request.RepositoryTaskStatusRunning + candidate.WorkerID = workerID + candidate.FencingToken = fencingToken + candidate.Attempt++ + candidate.LeaseExpiresAt = timePointer(leaseUntil) + candidate.LastHeartbeatAt = timePointer(now) + if candidate.StartedAt == nil { + candidate.StartedAt = timePointer(now) + } + candidate.ErrorCode = "" + candidate.ErrorMessage = "" + if err = repositoryDiagnosisService.Apply(candidate, nil); err != nil { + return nil, err + } + if err = s.tasks.Update(candidate, tx); err != nil { + return nil, err + } + return candidate, nil + } + return nil, nil +} + +func (s *service) Heartbeat(input request.HeartbeatRepositoryTaskRequest) ( + value *po.RepositoryTask, err error, +) { + if err = validateUUID("task_id", input.TaskID); err != nil { + return nil, err + } + if input.StorageNodeID != "" { + if err = validateUUID("storage_node_id", input.StorageNodeID); err != nil { + return nil, err + } + } + lease := time.Duration(input.LeaseSeconds) * time.Second + if err = validateWorkerAndLease(input.WorkerID, lease); err != nil { + return nil, err + } + if input.ProgressPercent < 0 || input.ProgressPercent > 100 { + return nil, fmt.Errorf("progress_percent must be between 0 and 100") + } + input.CurrentStep = strings.TrimSpace(input.CurrentStep) + if utf8.RuneCountInString(input.CurrentStep) > 128 || containsControl(input.CurrentStep) { + return nil, fmt.Errorf("current_step is invalid or exceeds 128 characters") + } + now := s.now() + leaseUntil := now.Add(lease) + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + value, err = s.tasks.GetByIDForUpdate(input.TaskID, tx) + if err != nil { + return nil, err + } + if err = verifyOwnership(value, input.WorkerID, input.FencingToken, now); err != nil { + return nil, err + } + if err = verifyStorageNodeAssignment(value, input.StorageNodeID); err != nil { + return nil, err + } + if value.RequiresMutationLock { + if err = s.locks.Renew(value.RepositoryID, value.ID, input.WorkerID, input.FencingToken, + now, leaseUntil, tx); err != nil { + return nil, ErrTaskOwnershipLost + } + } + value.LeaseExpiresAt = timePointer(leaseUntil) + value.LastHeartbeatAt = timePointer(now) + value.ProgressPercent = input.ProgressPercent + value.CurrentStep = repositoryDiagnosisService.Sanitize(input.CurrentStep) + if err = s.tasks.Update(value, tx); err != nil { + return nil, err + } + return value, nil +} + +func (s *service) Complete(input request.CompleteRepositoryTaskRequest) ( + value *po.RepositoryTask, err error, +) { + return s.CompleteWithMutation(input, nil) +} + +// CompleteWithMutation imports bounded failure metadata and closes the task in one transaction. +// Remote object reads and decompression must finish before this method is called. +func (s *service) CompleteWithMutation(input request.CompleteRepositoryTaskRequest, + mutate func(entity.Session, bool) error, +) (value *po.RepositoryTask, err error) { + if err = validateUUID("task_id", input.TaskID); err != nil { + return nil, err + } + if input.StorageNodeID != "" { + if err = validateUUID("storage_node_id", input.StorageNodeID); err != nil { + return nil, err + } + } + input.Status = strings.ToUpper(strings.TrimSpace(input.Status)) + if !isCompletionStatus(input.Status) { + return nil, fmt.Errorf("unsupported completion status %q", input.Status) + } + if err = validateTaskError(input.ErrorCode, input.ErrorMessage); err != nil { + return nil, err + } + input.ErrorCode = strings.TrimSpace(input.ErrorCode) + input.ErrorMessage = repositoryDiagnosisService.Sanitize(input.ErrorMessage) + input.ResultObjectID = strings.TrimSpace(input.ResultObjectID) + input.ResultObjectSHA256 = strings.ToLower(strings.TrimSpace(input.ResultObjectSHA256)) + hasFailureResult := input.ResultObjectID != "" || input.ResultObjectSHA256 != "" || input.QuarantineCount != 0 + if hasFailureResult && (input.Status != request.RepositoryTaskStatusFailed || + input.ResultObjectID == "" || len(input.ResultObjectID) > 512 || containsControl(input.ResultObjectID) || + !sha256HexPattern.MatchString(input.ResultObjectSHA256) || input.QuarantineCount <= 0) { + return nil, fmt.Errorf("repository failure result metadata is invalid") + } + now := s.now() + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + value, err = s.tasks.GetByIDForUpdate(input.TaskID, tx) + if err != nil { + return nil, err + } + if input.StorageNodeID != "" { + if err = verifyStorageNodeAssignment(value, input.StorageNodeID); err != nil { + return nil, err + } + } + if isCompletionStatus(value.Status) { + if value.Status == input.Status && value.ErrorCode == input.ErrorCode && + value.ErrorMessage == input.ErrorMessage && value.ResultObjectID == input.ResultObjectID && + value.ResultObjectSHA256 == input.ResultObjectSHA256 { + if mutate != nil { + if err = mutate(tx, true); err != nil { + return nil, err + } + } + return value, nil + } + return nil, ErrInvalidTaskTransition + } + if err = verifyOwnership(value, input.WorkerID, input.FencingToken, now); err != nil { + return nil, err + } + if (value.Status == request.RepositoryTaskStatusCancelRequested) != + (input.Status == request.RepositoryTaskStatusCanceled) { + return nil, ErrInvalidTaskTransition + } + if value.CommitState == request.RepositoryTaskCommitStateConfirmed && + input.Status != request.RepositoryTaskStatusSuccess { + return nil, ErrInvalidTaskTransition + } + if value.CommitState == request.RepositoryTaskCommitStatePrepared && + input.Status == request.RepositoryTaskStatusSuccess { + return nil, ErrInvalidTaskTransition + } + if input.Status == request.RepositoryTaskStatusPartialSuccess && value.Type != request.RepositoryTaskTypeUpload && + value.Type != request.RepositoryTaskTypeDistribute && value.Type != request.RepositoryTaskTypePackageInstall { + return nil, ErrInvalidTaskTransition + } + if mutate != nil { + if err = mutate(tx, false); err != nil { + return nil, err + } + } + if value.Type == request.RepositoryTaskTypeDeletePackage && + (input.Status == request.RepositoryTaskStatusFailed || input.Status == request.RepositoryTaskStatusCanceled) { + nevras, decodeErr := deletePackageTaskNEVRAs(value.PayloadJSON) + if decodeErr != nil { + return nil, fmt.Errorf("decode delete package result payload: %w", decodeErr) + } + if err = s.recordPackageDeleteResults(value, nevras, input.Status, input.ErrorCode, + input.ErrorMessage, tx); err != nil { + return nil, err + } + } + if value.RequiresMutationLock { + if err = s.locks.Release(value.RepositoryID, value.ID, input.WorkerID, input.FencingToken, now, tx); err != nil { + return nil, ErrTaskOwnershipLost + } + } + value.Status = input.Status + value.ErrorCode = input.ErrorCode + value.ErrorMessage = input.ErrorMessage + value.ResultObjectID = input.ResultObjectID + value.ResultObjectSHA256 = input.ResultObjectSHA256 + if value.CommitState == request.RepositoryTaskCommitStatePrepared { + clearPreparedCommit(value) + } + if err = repositoryDiagnosisService.Apply(value, repositoryDiagnosisService.Analyze( + repositoryDiagnosisService.Context{ + TaskType: value.Type, Status: input.Status, ErrorCode: value.ErrorCode, + ErrorMessage: value.ErrorMessage, CurrentStep: value.CurrentStep, RevisionID: value.RevisionID, + }, now)); err != nil { + return nil, err + } + value.LeaseExpiresAt = nil + value.FinishedAt = timePointer(now) + if input.Status == request.RepositoryTaskStatusSuccess || input.Status == request.RepositoryTaskStatusPartialSuccess { + value.ProgressPercent = 100 + } + if err = s.tasks.Update(value, tx); err != nil { + return nil, err + } + return value, nil +} + +// AttachRevision records the immutable output selected by a fenced worker. +// It is internal-only and must run before terminal task completion. +func (s *service) AttachRevision(input request.AttachRepositoryTaskRevisionRequest) ( + value *po.RepositoryTask, err error, +) { + if err = validateUUID("task_id", input.TaskID); err != nil { + return nil, err + } + if err = validateUUID("revision_id", input.RevisionID); err != nil { + return nil, err + } + now := s.now() + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + value, err = s.tasks.GetByIDForUpdate(input.TaskID, tx) + if err != nil { + return nil, err + } + if err = verifyOwnership(value, input.WorkerID, input.FencingToken, now); err != nil { + return nil, err + } + if value.RevisionID != "" && value.RevisionID != input.RevisionID { + return nil, fmt.Errorf("repository task already references another revision") + } + value.RevisionID = input.RevisionID + if err = s.tasks.Update(value, tx); err != nil { + return nil, err + } + return value, nil +} + +func (s *service) PrepareCommit(input request.PrepareRepositoryTaskCommitRequest) ( + value *po.RepositoryTask, err error, +) { + return s.PrepareCommitWithMutation(input, nil) +} + +// PrepareCommitWithMutation persists imported result metadata and PREPARED state atomically. +// The callback must only perform bounded database work; remote I/O must finish before this call. +func (s *service) PrepareCommitWithMutation(input request.PrepareRepositoryTaskCommitRequest, + mutate func(entity.Session, bool) error, +) (value *po.RepositoryTask, err error) { + if err = validateUUID("task_id", input.TaskID); err != nil { + return nil, err + } + if input.StorageNodeID != "" { + if err = validateUUID("storage_node_id", input.StorageNodeID); err != nil { + return nil, err + } + } + if err = validateUUID("revision_id", input.RevisionID); err != nil { + return nil, err + } + if err = validateWorkerID(input.WorkerID); err != nil { + return nil, err + } + input.ManifestSHA256 = strings.ToLower(strings.TrimSpace(input.ManifestSHA256)) + input.ResultObjectSHA256 = strings.ToLower(strings.TrimSpace(input.ResultObjectSHA256)) + input.ResultObjectID = strings.TrimSpace(input.ResultObjectID) + input.CompletionStatus = strings.TrimSpace(input.CompletionStatus) + if input.CompletionStatus == "" { + input.CompletionStatus = request.RepositoryTaskStatusSuccess + } + input.ErrorCode = strings.TrimSpace(input.ErrorCode) + input.ErrorMessage = repositoryDiagnosisService.Sanitize(input.ErrorMessage) + if input.CompletionStatus != request.RepositoryTaskStatusSuccess && + input.CompletionStatus != request.RepositoryTaskStatusPartialSuccess { + return nil, fmt.Errorf("prepared completion status must be SUCCESS or PARTIAL_SUCCESS") + } + if err = validateTaskError(input.ErrorCode, input.ErrorMessage); err != nil { + return nil, err + } + if input.CompletionStatus == request.RepositoryTaskStatusSuccess && + (input.ErrorCode != "" || input.ErrorMessage != "") { + return nil, fmt.Errorf("successful prepared commit must not contain an error summary") + } + if input.CompletionStatus == request.RepositoryTaskStatusPartialSuccess && input.ErrorCode == "" { + return nil, fmt.Errorf("partial prepared commit requires an error_code") + } + if !sha256HexPattern.MatchString(input.ManifestSHA256) || + !sha256HexPattern.MatchString(input.ResultObjectSHA256) { + return nil, fmt.Errorf("manifest and result object SHA-256 values must be lowercase hexadecimal") + } + if input.ResultObjectID == "" || len(input.ResultObjectID) > 512 || containsControl(input.ResultObjectID) { + return nil, fmt.Errorf("result_object_id is invalid or exceeds 512 characters") + } + now := s.now() + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + value, err = s.tasks.GetByIDForUpdate(input.TaskID, tx) + if err != nil { + return nil, err + } + if err = verifyOwnership(value, input.WorkerID, input.FencingToken, now); err != nil { + return nil, err + } + if err = verifyStorageNodeAssignment(value, input.StorageNodeID); err != nil { + return nil, err + } + if input.CompletionStatus == request.RepositoryTaskStatusPartialSuccess && + value.Type != request.RepositoryTaskTypeUpload { + return nil, ErrInvalidTaskTransition + } + if value.CommitState == request.RepositoryTaskCommitStatePrepared { + if value.PreparedRevisionID == input.RevisionID && value.PreparedManifestSHA256 == input.ManifestSHA256 && + value.ResultObjectID == input.ResultObjectID && value.ResultObjectSHA256 == input.ResultObjectSHA256 && + value.PreparedCompletionStatus == input.CompletionStatus && + value.PreparedErrorCode == input.ErrorCode && value.PreparedErrorMessage == input.ErrorMessage { + if mutate != nil { + if err = mutate(tx, true); err != nil { + return nil, err + } + } + return value, nil + } + return nil, ErrTaskCommitMismatch + } + if value.CommitState != "" && value.CommitState != request.RepositoryTaskCommitStateNone { + return nil, ErrInvalidTaskTransition + } + if mutate != nil { + if err = mutate(tx, false); err != nil { + return nil, err + } + } + digest := sha256.Sum256([]byte(strings.Join([]string{ + value.ID, fmt.Sprintf("%d", value.FencingToken), input.RevisionID, + input.ManifestSHA256, uuid.NewString(), + }, ":"))) + value.CommitState = request.RepositoryTaskCommitStatePrepared + value.CommitToken = fmt.Sprintf("%x", digest) + value.PreparedRevisionID = input.RevisionID + value.PreparedManifestSHA256 = input.ManifestSHA256 + value.ResultObjectID = input.ResultObjectID + value.ResultObjectSHA256 = input.ResultObjectSHA256 + value.PreparedCompletionStatus = input.CompletionStatus + value.PreparedErrorCode = input.ErrorCode + value.PreparedErrorMessage = input.ErrorMessage + value.PreparedAt = timePointer(now) + if err = s.tasks.Update(value, tx); err != nil { + return nil, err + } + return value, nil +} + +func (s *service) ConfirmCommit(input request.ConfirmRepositoryTaskCommitRequest) ( + value *po.RepositoryTask, err error, +) { + if err = validateUUID("task_id", input.TaskID); err != nil { + return nil, err + } + if input.StorageNodeID != "" { + if err = validateUUID("storage_node_id", input.StorageNodeID); err != nil { + return nil, err + } + } + if err = validateWorkerID(input.WorkerID); err != nil { + return nil, err + } + input.CommitToken = strings.ToLower(strings.TrimSpace(input.CommitToken)) + if !sha256HexPattern.MatchString(input.CommitToken) { + return nil, fmt.Errorf("commit_token must be a lowercase SHA-256 value") + } + now := s.now() + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + value, err = s.tasks.GetByIDForUpdate(input.TaskID, tx) + if err != nil { + return nil, err + } + if err = verifyStorageNodeAssignment(value, input.StorageNodeID); err != nil { + return nil, err + } + if value.CommitToken != input.CommitToken { + return nil, ErrTaskCommitMismatch + } + if value.CommitState == request.RepositoryTaskCommitStateConfirmed { + return value, nil + } + if err = verifyOwnership(value, input.WorkerID, input.FencingToken, now); err != nil { + return nil, err + } + if value.CommitState != request.RepositoryTaskCommitStatePrepared { + return nil, ErrInvalidTaskTransition + } + value.CommitState = request.RepositoryTaskCommitStateConfirmed + value.RevisionID = value.PreparedRevisionID + value.ActivatedAt = timePointer(now) + value.ConfirmedAt = timePointer(now) + if err = s.tasks.Update(value, tx); err != nil { + return nil, err + } + return value, nil +} + +// ConfirmAndCompleteCommit atomically activates the repository metadata, completes the task, +// and releases its mutation lock after the node has switched the immutable current link. +func (s *service) ConfirmAndCompleteCommit(input request.ConfirmRepositoryTaskCommitRequest) ( + value *po.RepositoryTask, err error, +) { + return s.ConfirmAndCompleteCommitWithMutation(input, nil) +} + +// ConfirmAndCompleteCommitWithMutation lets a product task replace the default +// repository activation with an equally short, transactional metadata change. +// Remote I/O and filesystem publication must finish before this method is called. +func (s *service) ConfirmAndCompleteCommitWithMutation(input request.ConfirmRepositoryTaskCommitRequest, + mutate func(entity.Session, *po.RepositoryTask, bool) error, +) (value *po.RepositoryTask, err error) { + if err = validateUUID("task_id", input.TaskID); err != nil { + return nil, err + } + if err = validateUUID("storage_node_id", input.StorageNodeID); err != nil { + return nil, err + } + if err = validateWorkerID(input.WorkerID); err != nil { + return nil, err + } + input.CommitToken = strings.ToLower(strings.TrimSpace(input.CommitToken)) + if !sha256HexPattern.MatchString(input.CommitToken) { + return nil, fmt.Errorf("commit_token must be a lowercase SHA-256 value") + } + now := s.now() + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + value, err = s.tasks.GetByIDForUpdate(input.TaskID, tx) + if err != nil { + return nil, err + } + if err = verifyStorageNodeAssignment(value, input.StorageNodeID); err != nil { + return nil, err + } + if value.CommitToken != input.CommitToken { + return nil, ErrTaskCommitMismatch + } + if value.CommitState == request.RepositoryTaskCommitStateConfirmed && + value.Status == value.PreparedCompletionStatus { + if mutate != nil { + if err = mutate(tx, value, true); err != nil { + return nil, err + } + } + return value, nil + } + if err = verifyOwnership(value, input.WorkerID, input.FencingToken, now); err != nil { + return nil, err + } + if value.CommitState != request.RepositoryTaskCommitStatePrepared || !value.RequiresMutationLock { + return nil, ErrInvalidTaskTransition + } + lock, err := s.locks.GetForUpdate(value.RepositoryID, tx) + if err != nil { + return nil, err + } + if lock.OwnerTaskID != value.ID || lock.WorkerID != input.WorkerID || + lock.FencingToken != input.FencingToken || lock.LeaseExpiresAt == nil || + !lock.LeaseExpiresAt.After(now) { + return nil, ErrTaskOwnershipLost + } + if mutate != nil { + if err = mutate(tx, value, false); err != nil { + return nil, err + } + } else { + if err = s.repositories.ActivateRevision(value.RepositoryID, value.PreparedRevisionID, + request.RepositoryStatusReady, tx); err != nil { + return nil, err + } + } + if err = s.locks.Release(value.RepositoryID, value.ID, input.WorkerID, input.FencingToken, now, tx); err != nil { + return nil, ErrTaskOwnershipLost + } + value.CommitState = request.RepositoryTaskCommitStateConfirmed + value.RevisionID = value.PreparedRevisionID + value.ActivatedAt = timePointer(now) + value.ConfirmedAt = timePointer(now) + value.Status = value.PreparedCompletionStatus + value.ErrorCode = value.PreparedErrorCode + value.ErrorMessage = value.PreparedErrorMessage + value.LeaseExpiresAt = nil + value.FinishedAt = timePointer(now) + value.ProgressPercent = 100 + value.CurrentStep = "published immutable revision" + var diagnosis *po.RepositoryDiagnosis + if value.Status == request.RepositoryTaskStatusPartialSuccess { + diagnosis = repositoryDiagnosisService.Analyze(repositoryDiagnosisService.Context{ + TaskType: value.Type, Status: value.Status, ErrorCode: value.ErrorCode, + ErrorMessage: value.ErrorMessage, CurrentStep: value.CurrentStep, + RevisionID: value.RevisionID, + }, now) + } + if err = repositoryDiagnosisService.Apply(value, diagnosis); err != nil { + return nil, err + } + if err = s.tasks.Update(value, tx); err != nil { + return nil, err + } + return value, nil +} + +func clearPreparedCommit(value *po.RepositoryTask) { + value.CommitState = request.RepositoryTaskCommitStateNone + value.CommitToken = "" + value.PreparedRevisionID = "" + value.PreparedManifestSHA256 = "" + value.ResultObjectID = "" + value.ResultObjectSHA256 = "" + value.PreparedCompletionStatus = request.RepositoryTaskStatusSuccess + value.PreparedErrorCode = "" + value.PreparedErrorMessage = "" + value.PreparedAt = nil + value.ActivatedAt = nil + value.ConfirmedAt = nil +} + +// ExecuteFencedMutation serializes the final active-state change with task +// cancellation, lease recovery, and lock reassignment. The callback must only +// contain the short database/filesystem publication commit, never downloads or +// metadata generation. +func (s *service) ExecuteFencedMutation(input request.ExecuteRepositoryMutationRequest, + mutate func(entity.Session) error, +) (err error) { + if err = validateUUID("task_id", input.TaskID); err != nil { + return err + } + if err = validateWorkerID(input.WorkerID); err != nil { + return err + } + if input.FencingToken == 0 { + return fmt.Errorf("fencing_token must be greater than zero") + } + if err = validateUUID("revision_id", input.RevisionID); err != nil { + return err + } + if mutate == nil { + return errors.New("repository mutation callback is required") + } + + now := s.now() + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + task, err := s.tasks.GetByIDForUpdate(input.TaskID, tx) + if err != nil { + return err + } + if err = verifyOwnership(task, input.WorkerID, input.FencingToken, now); err != nil { + return err + } + if task.Status != request.RepositoryTaskStatusRunning || !task.RequiresMutationLock { + return ErrInvalidTaskTransition + } + if task.RevisionID != "" && task.RevisionID != input.RevisionID { + return fmt.Errorf("repository task already references another revision") + } + lock, err := s.locks.GetForUpdate(task.RepositoryID, tx) + if err != nil { + return err + } + if lock.OwnerTaskID != task.ID || lock.WorkerID != input.WorkerID || + lock.FencingToken != input.FencingToken || lock.LeaseExpiresAt == nil || + !lock.LeaseExpiresAt.After(now) { + return ErrTaskOwnershipLost + } + if err = mutate(tx); err != nil { + return err + } + task.RevisionID = input.RevisionID + task.CurrentStep = "published immutable revision" + return s.tasks.Update(task, tx) +} + +func (s *service) RecoverExpired() (recovered int, err error) { + return s.recoverExpired(s.db) +} + +// RecoverExpiredContext bounds maintenance database work by its lifecycle context. +func (s *service) RecoverExpiredContext(ctx context.Context) (int, error) { + if err := ctx.Err(); err != nil { + return 0, err + } + return s.recoverExpired(contextSessionFactory{ctx: ctx, factory: s.db}) +} + +type contextSessionFactory struct { + ctx context.Context + factory entity.SessionFactory +} + +func (f contextSessionFactory) NewSession() entity.Session { return f.DB().Begin() } +func (f contextSessionFactory) DB() entity.Session { + return f.factory.DB().WithContext(f.ctx) +} + +func (s *service) recoverExpired(factory entity.SessionFactory) (recovered int, err error) { + now := s.now() + tx, maker := entity.NewSessionMaker(nil, factory) + defer maker.Close(&err) + values, err := s.tasks.FindExpired(now, recoveryBatchLimit, tx) + if err != nil { + return 0, err + } + for index := range values { + value := &values[index] + if value.RequiresMutationLock { + releaseErr := s.locks.Release(value.RepositoryID, value.ID, value.WorkerID, + value.FencingToken, now, tx) + if releaseErr != nil && !errors.Is(releaseErr, repositorylockrepo.ErrLockLost) { + return recovered, releaseErr + } + } + if value.Status == request.RepositoryTaskStatusCancelRequested { + value.Status = request.RepositoryTaskStatusCanceled + value.FinishedAt = timePointer(now) + value.ErrorCode = "CANCELED_AFTER_LEASE_EXPIRY" + value.ErrorMessage = "worker lease expired after cancellation was requested" + } else if value.Attempt < value.MaxAttempts { + value.Status = request.RepositoryTaskStatusPending + value.AvailableAt = now + value.ErrorCode = "LEASE_EXPIRED_RETRY" + value.ErrorMessage = "worker lease expired; task returned to the queue" + } else { + value.Status = request.RepositoryTaskStatusFailed + value.FinishedAt = timePointer(now) + value.ErrorCode = "LEASE_EXPIRED" + value.ErrorMessage = "worker lease expired and max attempts were exhausted" + } + var diagnosis *po.RepositoryDiagnosis + if value.Status == request.RepositoryTaskStatusFailed || value.Status == request.RepositoryTaskStatusCanceled { + diagnosis = repositoryDiagnosisService.Analyze(repositoryDiagnosisService.Context{ + TaskType: value.Type, Status: value.Status, ErrorCode: value.ErrorCode, + ErrorMessage: value.ErrorMessage, CurrentStep: value.CurrentStep, RevisionID: value.RevisionID, + }, now) + } + if err = repositoryDiagnosisService.Apply(value, diagnosis); err != nil { + return recovered, err + } + value.WorkerID = "" + value.FencingToken = 0 + value.LeaseExpiresAt = nil + if err = s.tasks.Update(value, tx); err != nil { + return recovered, err + } + recovered++ + } + return recovered, nil +} + +func (s *service) authorizeRepository(ctx entity.Context, repositoryID string, session entity.Session) ( + *po.SoftwareRepository, error, +) { + if err := validateUUID("repository_id", repositoryID); err != nil { + return nil, err + } + if ctx.User == nil || strings.TrimSpace(ctx.User.LoginName) == "" { + return nil, errors.New("authenticated user is required") + } + repository, err := s.repositories.GetByID(repositoryID, session) + if err != nil { + return nil, err + } + project, err := s.projects.DescribeProject(ctx, request.DescribeProjectRequest{ID: repository.ProjectID}, session) + if err != nil { + return nil, fmt.Errorf("project %d not found: %w", repository.ProjectID, err) + } + for _, role := range ctx.User.Roles { + if role.RoleId == request.DefaultGlobalRoleName { + return repository, nil + } + } + if project.Project.Creator != ctx.User.LoginName { + return nil, errors.New("user cannot manage repository tasks in this project") + } + return repository, nil +} + +func normalizeEnqueueRequest(input request.EnqueueRepositoryTaskRequest) (request.EnqueueRepositoryTaskRequest, error) { + if err := validateUUID("repository_id", input.RepositoryID); err != nil { + return input, err + } + input.Type = strings.ToUpper(strings.TrimSpace(input.Type)) + if !isTaskType(input.Type) { + return input, fmt.Errorf("unsupported repository task type %q", input.Type) + } + input.RevisionID = strings.TrimSpace(input.RevisionID) + input.PreflightID = strings.TrimSpace(input.PreflightID) + if input.PreflightID != "" { + if err := validateUUID("preflight_id", input.PreflightID); err != nil { + return input, err + } + } + if input.Type == request.RepositoryTaskTypeValidate || input.Type == request.RepositoryTaskTypeDistribute { + if err := validateUUID("revision_id", input.RevisionID); err != nil { + return input, err + } + } else if input.RevisionID != "" { + if err := validateUUID("revision_id", input.RevisionID); err != nil { + return input, err + } + } + input.PayloadJSON = strings.TrimSpace(input.PayloadJSON) + if input.PayloadJSON == "" { + input.PayloadJSON = "{}" + } + if len(input.PayloadJSON) > maxTaskPayloadBytes || !json.Valid([]byte(input.PayloadJSON)) { + return input, fmt.Errorf("task payload must be valid JSON and no larger than %d bytes", maxTaskPayloadBytes) + } + if input.MaxAttempts == 0 { + input.MaxAttempts = defaultMaxAttempts + } + if input.MaxAttempts < 1 || input.MaxAttempts > 10 { + return input, fmt.Errorf("max_attempts must be between 1 and 10") + } + normalizedTrigger, err := normalizeTaskTrigger(input.Trigger, true) + if err != nil { + return input, err + } + input.Trigger = normalizedTrigger + input.IdempotencyKey = strings.TrimSpace(input.IdempotencyKey) + if len(input.IdempotencyKey) > 128 || containsControl(input.IdempotencyKey) { + return input, fmt.Errorf("idempotency_key is invalid or exceeds 128 characters") + } + if input.BandwidthLimitBytes < 0 { + return input, fmt.Errorf("bandwidth_limit_bytes cannot be negative") + } + if input.MaxParallelDownloads < 0 || input.MaxParallelDownloads > 20 { + return input, fmt.Errorf("max_parallel_downloads must be between 0 and 20") + } + return input, nil +} + +func normalizeTaskTrigger(value string, defaultManual bool) (string, error) { + value = strings.ToUpper(strings.TrimSpace(value)) + if value == "" && defaultManual { + return request.RepositoryTaskTriggerManual, nil + } + switch value { + case "", request.RepositoryTaskTriggerManual, request.RepositoryTaskTriggerScheduled, + request.RepositoryTaskTriggerRetry, request.RepositoryTaskTriggerSystem: + return value, nil + default: + return "", fmt.Errorf("unsupported repository task trigger %q", value) + } +} + +func verifyOwnership(value *po.RepositoryTask, workerID string, fencingToken uint64, now time.Time) error { + if value.Status != request.RepositoryTaskStatusRunning && + value.Status != request.RepositoryTaskStatusCancelRequested { + return ErrTaskNotRunnable + } + if value.WorkerID != workerID || value.FencingToken != fencingToken { + return ErrTaskOwnershipLost + } + if value.LeaseExpiresAt == nil || !value.LeaseExpiresAt.After(now) { + return ErrTaskLeaseExpired + } + return nil +} + +func verifyStorageNodeAssignment(value *po.RepositoryTask, storageNodeID string) error { + if storageNodeID != "" && value.StorageNodeID != storageNodeID { + return ErrTaskStorageNodeMismatch + } + return nil +} + +func validateWorkerAndLease(workerID string, lease time.Duration) error { + if err := validateWorkerID(workerID); err != nil { + return err + } + if lease < minLeaseDuration || lease > maxLeaseDuration { + return fmt.Errorf("lease must be between %s and %s", minLeaseDuration, maxLeaseDuration) + } + return nil +} + +func validateWorkerID(workerID string) error { + workerID = strings.TrimSpace(workerID) + if workerID == "" || len(workerID) > 128 || !workerIDPattern.MatchString(workerID) { + return fmt.Errorf("worker_id is invalid") + } + return nil +} + +func validateTaskError(code, message string) error { + code = strings.TrimSpace(code) + message = strings.TrimSpace(message) + if utf8.RuneCountInString(code) > 64 || containsControl(code) { + return fmt.Errorf("error_code is invalid or exceeds 64 characters") + } + if utf8.RuneCountInString(message) > 2048 || containsControl(message) { + return fmt.Errorf("error_message is invalid or exceeds 2048 characters") + } + return nil +} + +func preflightErrorDetail(err error) (string, string) { + if err == nil { + return "", "" + } + type coded interface{ Code() string } + code := "REPOSITORY_NODE_PRECHECK_STALE" + if value, ok := err.(coded); ok && strings.TrimSpace(value.Code()) != "" { + code = strings.TrimSpace(value.Code()) + } + message := strings.TrimSpace(err.Error()) + if prefix := code + ": "; strings.HasPrefix(message, prefix) { + message = strings.TrimPrefix(message, prefix) + } + if utf8.RuneCountInString(message) > 2048 { + message = string([]rune(message)[:2048]) + } + return code, message +} + +func validateUUID(field, value string) error { + if _, err := uuid.Parse(strings.TrimSpace(value)); err != nil { + return fmt.Errorf("invalid %s: %w", field, err) + } + return nil +} + +func requiresMutationLock(taskType string) bool { + return taskType != request.RepositoryTaskTypeValidate && taskType != request.RepositoryTaskTypeDistribute && + taskType != request.RepositoryTaskTypeNodeInstall && taskType != request.RepositoryTaskTypePackageInstall && + taskType != request.RepositoryTaskTypeInventoryImport +} + +func isTaskType(value string) bool { + switch value { + case request.RepositoryTaskTypeSync, request.RepositoryTaskTypeUpload, + request.RepositoryTaskTypeDeletePackage, request.RepositoryTaskTypeRebuildMetadata, + request.RepositoryTaskTypeDeleteRepository, request.RepositoryTaskTypePublish, + request.RepositoryTaskTypeValidate, request.RepositoryTaskTypeDistribute, + request.RepositoryTaskTypeMigrateRepository, request.RepositoryTaskTypeNodeInstall, + request.RepositoryTaskTypePackageInstall, request.RepositoryTaskTypeInventoryImport: + return true + default: + return false + } +} + +func isTaskStatus(value string) bool { + switch value { + case request.RepositoryTaskStatusPending, request.RepositoryTaskStatusRunning, + request.RepositoryTaskStatusCancelRequested, request.RepositoryTaskStatusCanceled, + request.RepositoryTaskStatusSuccess, request.RepositoryTaskStatusFailed, + request.RepositoryTaskStatusPartialSuccess: + return true + default: + return false + } +} + +func isCompletionStatus(value string) bool { + return value == request.RepositoryTaskStatusSuccess || value == request.RepositoryTaskStatusFailed || + value == request.RepositoryTaskStatusPartialSuccess || value == request.RepositoryTaskStatusCanceled +} + +func containsControl(value string) bool { + return strings.IndexFunc(value, unicode.IsControl) >= 0 +} + +func timePointer(value time.Time) *time.Time { + return &value +} + +func optionalString(value string) *string { + if value == "" { + return nil + } + return &value +} diff --git a/manager/backend/services/footstone/domain/repositoryTaskService/service_integration_test.go b/manager/backend/services/footstone/domain/repositoryTaskService/service_integration_test.go new file mode 100644 index 0000000000000000000000000000000000000000..bfd132856d76479fd1ab6dfc8bca3943a82a937e --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryTaskService/service_integration_test.go @@ -0,0 +1,950 @@ +//go:build integration + +package repositoryTaskService + +import ( + "crypto/sha256" + "encoding/hex" + "errors" + "os" + "strings" + "sync" + "testing" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + ftpb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/footstone" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/infrarepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorycontentrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorylockrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositoryrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorystoragenoderepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorytaskrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryNodeService" +) + +const ( + integrationProjectID int64 = 920002 + integrationRepoOne = "22222222-2222-2222-2222-222222222221" + integrationRepoTwo = "22222222-2222-2222-2222-222222222222" + integrationRepoMigration = "22222222-2222-4222-8222-222222222223" + integrationRevision = "33333333-3333-3333-3333-333333333333" + integrationRevisionAtomic = "33333333-3333-4333-8333-333333333334" + integrationUploadFailure = "55555555-5555-4555-8555-555555555551" + integrationNodeOne = "44444444-4444-4444-8444-444444444441" + integrationNodeTwo = "44444444-4444-4444-8444-444444444442" + preflightProjectID int64 = 920006 + preflightRepositoryID = "abababab-abab-4bab-8bab-abababababab" + preflightNodeID = "cdcdcdcd-cdcd-4dcd-8dcd-cdcdcdcdcdcd" +) + +func TestTaskPreflightBindingAndClaimGateWithMySQL(t *testing.T) { + dsn := os.Getenv("OCM_REPOSITORY_TEST_DSN") + if dsn == "" { + t.Skip("OCM_REPOSITORY_TEST_DSN is not set") + } + db, err := database.NewDBClient(config.AccessPoint{Source: dsn}) + if err != nil { + t.Fatal(err) + } + defer db.Close() + if err = db.DB().AutoMigrate(&po.Project{}); err != nil { + t.Fatal(err) + } + repositories := repositoryrepo.New(db) + tasks := repositorytaskrepo.New(db) + locks := repositorylockrepo.New(db) + service := newService(db, repositories, tasks, locks, infrarepo.NewInfrastructureRepository(db)) + nodeService := repositoryNodeService.New(db) + content := repositorycontentrepo.New(db) + if err = repositories.SyncTables(); err != nil { + t.Fatal(err) + } + if err = service.SyncTables(); err != nil { + t.Fatal(err) + } + if err = nodeService.SyncTables(); err != nil { + t.Fatal(err) + } + if err = content.SyncTables(); err != nil { + t.Fatal(err) + } + cleanupPreflightIntegrationRows(db) + defer cleanupPreflightIntegrationRows(db) + + if err = db.DB().Create(&po.Project{Base: po.Base{ID: preflightProjectID}, + Name: "repository-preflight", EnName: "repository-preflight", Creator: "repository-admin"}).Error; err != nil { + t.Fatal(err) + } + now := time.Now().UTC() + tools := []byte(`{"rpm":"4.16","rpmkeys":"4.16","dnf":"4.14","createrepo_c":"0.20"}`) + nodeRepository := repositorystoragenoderepo.New(db) + node := &po.RepositoryStorageNode{ID: preflightNodeID, ProjectID: preflightProjectID, + Name: "repository-preflight-node", ControlBaseURL: "https://repository-node.invalid:19443", + PublishBaseURL: "http://repository-node.invalid:18090", CertificateFingerprint: strings.Repeat("a", 64), + Status: request.RepositoryStorageNodeStatusOnline, Enabled: true, FilesystemType: "xfs", + CapacityBytes: 1000, FreeBytes: 500, FreeInodes: 20000, ToolVersionsJSON: tools, + LabelsJSON: []byte(`{}`), LastHeartbeatAt: &now, CreatedBy: "repository-admin"} + if err = nodeRepository.CreateNode(node, nil); err != nil { + t.Fatal(err) + } + repository := &po.SoftwareRepository{ID: preflightRepositoryID, ProjectID: preflightProjectID, + SystemProduct: "OpenCloudOS", ProductVersion: "9.6", Name: "preflight-private", + Architecture: request.RepositoryArchitectureX8664, Type: request.RepositoryTypePrivate, + StorageLocation: "public/preflight-private", PublishKey: "preflight-private", + AllowUpload: true, IncludePackagesJSON: "[]", ExcludePackagesJSON: "[]", + GPGPolicy: request.RepositoryGPGDisabled, Enabled: true, Status: request.RepositoryStatusEmpty, + PrimaryStorageNodeID: preflightNodeID, PublishBaseURLSnapshot: node.PublishBaseURL, + Creator: "repository-admin"} + if err = repositories.Create(repository, nil); err != nil { + t.Fatal(err) + } + policy := request.RepositoryPreflightPolicy{MaxExpandedBytes: 200, MaxSyncBytes: 100, + MaxSyncPackages: 2000, MaxArchiveFiles: 50, MetadataReserve: 10, MinimumFreeBytes: 1, + SafetyPercent: 20, HardUsagePercent: 90, TTL: 5 * time.Minute} + nodeService.ConfigurePreflight(policy) + service.ConfigurePreflight(nodeService) + ctx := taskIntegrationContext() + + passed, err := service.Enqueue(ctx, request.EnqueueRepositoryTaskRequest{ + RepositoryID: repository.ID, Type: request.RepositoryTaskTypeSync, PayloadJSON: `{}`}) + if err != nil || passed.Status != request.RepositoryTaskStatusPending || passed.PreflightID == "" { + t.Fatalf("PASS enqueue = %#v/%v", passed, err) + } + record, err := nodeRepository.GetPreflightByID(passed.PreflightID, nil) + if err != nil || record.Status != request.RepositoryPreflightStatusPass || record.RequiredFreeBytes != 252 { + t.Fatalf("PASS preflight = %#v/%v", record, err) + } + claimed, err := service.ClaimNextForNode(node.ID, "preflight-worker", 30*time.Second) + assertClaim(t, claimed, err, passed.ID, true) + completeIntegrationTask(t, service, claimed, "preflight-worker", request.RepositoryTaskStatusSuccess) + + node.FreeBytes = 300 + node.LastHeartbeatAt = &now + if err = nodeRepository.UpdateNode(node, nil); err != nil { + t.Fatal(err) + } + rejected, err := service.Enqueue(ctx, request.EnqueueRepositoryTaskRequest{ + RepositoryID: repository.ID, Type: request.RepositoryTaskTypeSync, PayloadJSON: `{}`}) + if err != nil || rejected.Status != request.RepositoryTaskStatusFailed || + rejected.ErrorCode != "REPOSITORY_NODE_INSUFFICIENT_SPACE" { + t.Fatalf("insufficient-space enqueue = %#v/%v", rejected, err) + } + if claimed, err = service.ClaimNextForNode(node.ID, "preflight-worker", 30*time.Second); err != nil || claimed != nil { + t.Fatalf("rejected task was claimable = %#v/%v", claimed, err) + } + + node.FreeBytes, node.LastHeartbeatAt = 500, &now + if err = nodeRepository.UpdateNode(node, nil); err != nil { + t.Fatal(err) + } + stale, err := service.Enqueue(ctx, request.EnqueueRepositoryTaskRequest{ + RepositoryID: repository.ID, Type: request.RepositoryTaskTypeSync, PayloadJSON: `{}`}) + if err != nil || stale.Status != request.RepositoryTaskStatusPending { + t.Fatalf("stale candidate enqueue = %#v/%v", stale, err) + } + stalePreflightID := stale.PreflightID + if err = db.DB().Model(&po.RepositoryPreflightRecord{}).Where("id = ?", stalePreflightID). + Update("expires_at", time.Now().UTC().Add(-time.Second)).Error; err != nil { + t.Fatal(err) + } + claimed, err = service.ClaimNextForNode(node.ID, "preflight-worker", 30*time.Second) + if err != nil || claimed == nil || claimed.ID != stale.ID || claimed.PreflightID == stalePreflightID { + t.Fatalf("refreshed preflight claim = %#v/%v", claimed, err) + } + refreshed, err := nodeRepository.GetPreflightByID(claimed.PreflightID, nil) + if err != nil || refreshed.TaskID != stale.ID || refreshed.RequiredFreeBytes != 252 || + !refreshed.ExpiresAt.After(time.Now().UTC()) { + t.Fatalf("refreshed preflight = %#v/%v", refreshed, err) + } + completeIntegrationTask(t, service, claimed, "preflight-worker", request.RepositoryTaskStatusSuccess) +} + +func TestTaskLeaseLockAndRecoveryWithMySQL(t *testing.T) { + dsn := os.Getenv("OCM_REPOSITORY_TEST_DSN") + if dsn == "" { + t.Skip("OCM_REPOSITORY_TEST_DSN is not set") + } + db, err := database.NewDBClient(config.AccessPoint{Source: dsn}) + if err != nil { + t.Fatalf("connect test MySQL: %v", err) + } + defer db.Close() + if err = db.DB().AutoMigrate(&po.Project{}); err != nil { + t.Fatalf("create disposable project table: %v", err) + } + cleanupTaskIntegrationRows(db) + defer cleanupTaskIntegrationRows(db) + + project := po.Project{ + Base: po.Base{ID: integrationProjectID}, + Name: "repository-phase2-integration", + EnName: "repository-phase2-integration", + Creator: "repository-admin", + } + if err = db.DB().Create(&project).Error; err != nil { + t.Fatalf("create project: %v", err) + } + repositories := repositoryrepo.New(db) + tasks := repositorytaskrepo.New(db) + locks := repositorylockrepo.New(db) + service := newService(db, repositories, tasks, locks, infrarepo.NewInfrastructureRepository(db)) + if err = repositories.SyncTables(); err != nil { + t.Fatalf("create disposable repository table: %v", err) + } + if err = service.SyncTables(); err != nil { + t.Fatalf("create disposable task tables: %v", err) + } + if err = repositorystoragenoderepo.New(db).SyncTables(); err != nil { + t.Fatalf("create disposable storage node tables: %v", err) + } + if err = db.DB().AutoMigrate(&po.RepositoryRevision{}, &po.RepositoryPackage{}); err != nil { + t.Fatalf("create disposable repository content tables: %v", err) + } + createIntegrationRepository(t, db, integrationRepoOne, "repo-one", integrationNodeOne) + createIntegrationRepository(t, db, integrationRepoTwo, "repo-two", integrationNodeTwo) + createIntegrationRepository(t, db, integrationRepoMigration, "repo-migration", "") + now := time.Date(2026, 8, 20, 14, 0, 0, 0, time.UTC) + service.now = func() time.Time { return now } + ctx := taskIntegrationContext() + + taskA := enqueueIntegrationTask(t, service, ctx, integrationRepoOne, request.RepositoryTaskTypeSync, "", 2) + taskB := enqueueIntegrationTask(t, service, ctx, integrationRepoOne, request.RepositoryTaskTypeUpload, "", 2) + taskC := enqueueIntegrationTask(t, service, ctx, integrationRepoTwo, request.RepositoryTaskTypeSync, "", 2) + taskV := enqueueIntegrationTask(t, service, ctx, integrationRepoOne, request.RepositoryTaskTypeValidate, + integrationRevision, 2) + + claimedC, err := service.ClaimNextForNode(integrationNodeTwo, "worker-c", 10*time.Second) + assertClaim(t, claimedC, err, taskC.ID, true) + if claimedC.StorageNodeID != integrationNodeTwo { + t.Fatalf("node-two claimed storage node = %s", claimedC.StorageNodeID) + } + claimedA, err := service.ClaimNextForNode(integrationNodeOne, "worker-a", 10*time.Second) + assertClaim(t, claimedA, err, taskA.ID, true) + if claimedA.StorageNodeID != integrationNodeOne { + t.Fatalf("node-one claimed storage node = %s", claimedA.StorageNodeID) + } + mutationCalled := false + if err = service.ExecuteFencedMutation(request.ExecuteRepositoryMutationRequest{ + TaskID: claimedA.ID, WorkerID: "worker-a", FencingToken: claimedA.FencingToken, + RevisionID: integrationRevision, + }, func(entity.Session) error { + mutationCalled = true + return nil + }); err != nil || !mutationCalled { + t.Fatalf("ExecuteFencedMutation() called/error = %v/%v", mutationCalled, err) + } + mutationCalled = false + if err = service.ExecuteFencedMutation(request.ExecuteRepositoryMutationRequest{ + TaskID: claimedA.ID, WorkerID: "worker-a", FencingToken: claimedA.FencingToken + 1, + RevisionID: integrationRevision, + }, func(entity.Session) error { + mutationCalled = true + return nil + }); !errors.Is(err, ErrTaskOwnershipLost) || mutationCalled { + t.Fatalf("ExecuteFencedMutation(stale) called/error = %v/%v", mutationCalled, err) + } + if _, err = service.Cancel(ctx, claimedA.ID); !errors.Is(err, ErrTaskNotCancelable) { + t.Fatalf("Cancel(published task) error = %v", err) + } + claimedV, err := service.ClaimNextForNode(integrationNodeOne, "worker-v", 10*time.Second) + assertClaim(t, claimedV, err, taskV.ID, false) + if claimedV.FencingToken != 0 { + t.Fatalf("read-only task fencing token = %d, want 0", claimedV.FencingToken) + } + t.Logf("initial claims: repo1_token=%d repo2_token=%d validation_token=%d", + claimedA.FencingToken, claimedC.FencingToken, claimedV.FencingToken) + + now = now.Add(2 * time.Second) + if _, err = service.Heartbeat(request.HeartbeatRepositoryTaskRequest{ + TaskID: taskA.ID, StorageNodeID: integrationNodeOne, + WorkerID: "worker-a", FencingToken: claimedA.FencingToken, + LeaseSeconds: 10, ProgressPercent: 25, CurrentStep: "download metadata", + }); err != nil { + t.Fatalf("Heartbeat() error = %v", err) + } + if _, err = service.Heartbeat(request.HeartbeatRepositoryTaskRequest{ + TaskID: taskA.ID, StorageNodeID: integrationNodeTwo, + WorkerID: "worker-a", FencingToken: claimedA.FencingToken, LeaseSeconds: 10, + }); !errors.Is(err, ErrTaskStorageNodeMismatch) { + t.Fatalf("Heartbeat(wrong storage node) error = %v", err) + } + if _, err = service.Heartbeat(request.HeartbeatRepositoryTaskRequest{ + TaskID: taskA.ID, StorageNodeID: integrationNodeOne, + WorkerID: "worker-a", FencingToken: claimedA.FencingToken - 1, + LeaseSeconds: 10, + }); !errors.Is(err, ErrTaskOwnershipLost) { + t.Fatalf("Heartbeat(stale token) error = %v", err) + } + completeIntegrationTask(t, service, claimedV, "worker-v", request.RepositoryTaskStatusSuccess) + completeIntegrationTask(t, service, claimedC, "worker-c", request.RepositoryTaskStatusSuccess) + if value, err := service.ClaimNext("worker-blocked", 10*time.Second); err != nil || value != nil { + t.Fatalf("ClaimNext(same locked repository) value/error = %#v/%v", value, err) + } + completeIntegrationTask(t, service, claimedA, "worker-a", request.RepositoryTaskStatusSuccess) + claimedB, err := service.ClaimNext("worker-b", 10*time.Second) + assertClaim(t, claimedB, err, taskB.ID, true) + if claimedB.FencingToken <= claimedA.FencingToken { + t.Fatalf("fencing token did not increase: old=%d new=%d", claimedA.FencingToken, claimedB.FencingToken) + } + completeIntegrationTask(t, service, claimedB, "worker-b", request.RepositoryTaskStatusPartialSuccess) + + commitTask := enqueueIntegrationTask(t, service, ctx, integrationRepoTwo, + request.RepositoryTaskTypeSync, "", 2) + claimedCommit, err := service.ClaimNextForNode(integrationNodeTwo, "worker-commit", 10*time.Second) + assertClaim(t, claimedCommit, err, commitTask.ID, true) + manifestSHA := strings.Repeat("a", 64) + resultSHA := strings.Repeat("b", 64) + prepareInput := request.PrepareRepositoryTaskCommitRequest{ + TaskID: claimedCommit.ID, WorkerID: claimedCommit.WorkerID, + FencingToken: claimedCommit.FencingToken, RevisionID: integrationRevision, + StorageNodeID: integrationNodeTwo, + ManifestSHA256: manifestSHA, ResultObjectID: "task-results/commit/result.jsonl.zst", + ResultObjectSHA256: resultSHA, + } + prepared, err := service.PrepareCommit(prepareInput) + if err != nil || prepared.CommitState != request.RepositoryTaskCommitStatePrepared || + prepared.CommitToken == "" || prepared.PreparedRevisionID != integrationRevision { + t.Fatalf("PrepareCommit() task/error = %#v/%v", prepared, err) + } + preparedAgain, err := service.PrepareCommit(prepareInput) + if err != nil || preparedAgain.CommitToken != prepared.CommitToken { + t.Fatalf("PrepareCommit(idempotent) task/error = %#v/%v", preparedAgain, err) + } + mismatch := prepareInput + mismatch.ManifestSHA256 = strings.Repeat("c", 64) + if _, err = service.PrepareCommit(mismatch); !errors.Is(err, ErrTaskCommitMismatch) { + t.Fatalf("PrepareCommit(mismatch) error = %v", err) + } + confirmInput := request.ConfirmRepositoryTaskCommitRequest{ + TaskID: claimedCommit.ID, WorkerID: claimedCommit.WorkerID, + StorageNodeID: integrationNodeTwo, FencingToken: claimedCommit.FencingToken, + CommitToken: prepared.CommitToken, + } + confirmed, err := service.ConfirmCommit(confirmInput) + if err != nil || confirmed.CommitState != request.RepositoryTaskCommitStateConfirmed || + confirmed.RevisionID != integrationRevision || confirmed.ConfirmedAt == nil { + t.Fatalf("ConfirmCommit() task/error = %#v/%v", confirmed, err) + } + confirmedAgain, err := service.ConfirmCommit(confirmInput) + if err != nil || confirmedAgain.CommitState != request.RepositoryTaskCommitStateConfirmed { + t.Fatalf("ConfirmCommit(idempotent) task/error = %#v/%v", confirmedAgain, err) + } + completeIntegrationTask(t, service, claimedCommit, "worker-commit", request.RepositoryTaskStatusSuccess) + + atomicTask := enqueueIntegrationTask(t, service, ctx, integrationRepoTwo, + request.RepositoryTaskTypeSync, "", 2) + claimedAtomic, err := service.ClaimNextForNode(integrationNodeTwo, "worker-atomic", 10*time.Second) + assertClaim(t, claimedAtomic, err, atomicTask.ID, true) + atomicPrepare := request.PrepareRepositoryTaskCommitRequest{ + TaskID: claimedAtomic.ID, WorkerID: claimedAtomic.WorkerID, FencingToken: claimedAtomic.FencingToken, + RevisionID: integrationRevisionAtomic, StorageNodeID: integrationNodeTwo, + ManifestSHA256: strings.Repeat("d", 64), ResultObjectID: "task-results/atomic/result.jsonl.zst", + ResultObjectSHA256: strings.Repeat("e", 64), + } + rollbackMarker := errors.New("rollback imported revision") + if _, err = service.PrepareCommitWithMutation(atomicPrepare, func(tx entity.Session, _ bool) error { + if createErr := tx.Create(integrationRevisionFixture(claimedAtomic)).Error; createErr != nil { + return createErr + } + return rollbackMarker + }); !errors.Is(err, rollbackMarker) { + t.Fatalf("PrepareCommitWithMutation(rollback) error = %v", err) + } + var rolledBackCount int64 + if err = db.DB().Model(&po.RepositoryRevision{}).Where("id = ?", integrationRevisionAtomic). + Count(&rolledBackCount).Error; err != nil || rolledBackCount != 0 { + t.Fatalf("rolled back revision count/error = %d/%v", rolledBackCount, err) + } + afterRollback, err := service.Describe(ctx, claimedAtomic.ID) + if err != nil || (afterRollback.CommitState != "" && + afterRollback.CommitState != request.RepositoryTaskCommitStateNone) { + t.Fatalf("task commit state after rollback = %#v/%v", afterRollback, err) + } + preparedAtomic, err := service.PrepareCommitWithMutation(atomicPrepare, func(tx entity.Session, _ bool) error { + return tx.Create(integrationRevisionFixture(claimedAtomic)).Error + }) + if err != nil || preparedAtomic.CommitState != request.RepositoryTaskCommitStatePrepared { + t.Fatalf("PrepareCommitWithMutation() task/error = %#v/%v", preparedAtomic, err) + } + confirmedAtomic, err := service.ConfirmAndCompleteCommit(request.ConfirmRepositoryTaskCommitRequest{ + TaskID: claimedAtomic.ID, StorageNodeID: integrationNodeTwo, WorkerID: claimedAtomic.WorkerID, + FencingToken: claimedAtomic.FencingToken, CommitToken: preparedAtomic.CommitToken, + }) + if err != nil || confirmedAtomic.Status != request.RepositoryTaskStatusSuccess || + confirmedAtomic.CommitState != request.RepositoryTaskCommitStateConfirmed { + t.Fatalf("ConfirmAndCompleteCommit() task/error = %#v/%v", confirmedAtomic, err) + } + activeRepository, err := repositories.GetByID(integrationRepoTwo, nil) + if err != nil || activeRepository.ActiveRevisionID != integrationRevisionAtomic { + t.Fatalf("atomic active repository/error = %#v/%v", activeRepository, err) + } + confirmedAtomicAgain, err := service.ConfirmAndCompleteCommit(request.ConfirmRepositoryTaskCommitRequest{ + TaskID: claimedAtomic.ID, StorageNodeID: integrationNodeTwo, WorkerID: claimedAtomic.WorkerID, + FencingToken: claimedAtomic.FencingToken, CommitToken: preparedAtomic.CommitToken, + }) + if err != nil || confirmedAtomicAgain.Status != request.RepositoryTaskStatusSuccess { + t.Fatalf("ConfirmAndCompleteCommit(idempotent) task/error = %#v/%v", confirmedAtomicAgain, err) + } + t.Logf("atomic remote commit: revision=%s status=%s", activeRepository.ActiveRevisionID, + confirmedAtomic.Status) + + contentRepository := repositorycontentrepo.New(db) + if err = contentRepository.SyncTables(); err != nil { + t.Fatalf("create disposable repository upload tables: %v", err) + } + failureTask := enqueueIntegrationTask(t, service, ctx, integrationRepoTwo, + request.RepositoryTaskTypeUpload, "", 2) + upload := &po.RepositoryUpload{ID: integrationUploadFailure, RepositoryID: integrationRepoTwo, + ProjectID: integrationProjectID, StorageNodeID: integrationNodeTwo, TaskID: failureTask.ID, + FileName: "damaged.rpm", Kind: request.RepositoryUploadKindRPM, ExpectedSize: 7, ReceivedSize: 7, + ActualSHA256: strings.Repeat("a", 64), RelativePath: "staging/uploads/" + integrationUploadFailure + "/content", + Status: request.RepositoryUploadStatusReady, RequestedBy: "repository-admin", ExpiresAt: now.Add(time.Hour)} + if err = contentRepository.CreateUpload(upload, nil); err != nil { + t.Fatalf("create failed-upload fixture: %v", err) + } + claimedFailure, err := service.ClaimNextForNode(integrationNodeTwo, "worker-failure", 10*time.Second) + assertClaim(t, claimedFailure, err, failureTask.ID, true) + snapshot := request.RepositoryUploadSnapshot{UploadID: upload.ID, FileName: upload.FileName, + Kind: upload.Kind, RelativePath: upload.RelativePath, SizeBytes: upload.ExpectedSize, + SHA256: upload.ActualSHA256} + quarantine := []po.RepositoryQuarantineItem{{ID: "66666666-6666-4666-8666-666666666661", + RepositoryID: integrationRepoTwo, TaskID: claimedFailure.ID, UploadID: upload.ID, + OriginalName: upload.FileName, RelativePath: "quarantine/" + integrationRepoTwo + "/" + + claimedFailure.ID + "/damaged.rpm", SizeBytes: 7, SHA256: upload.ActualSHA256, + ReasonCode: request.RepositoryQuarantineDamagedRPM, ReasonMessage: "rpm integrity verification failed"}} + failureInput := request.CompleteRepositoryTaskRequest{TaskID: claimedFailure.ID, + StorageNodeID: integrationNodeTwo, WorkerID: claimedFailure.WorkerID, + FencingToken: claimedFailure.FencingToken, Status: request.RepositoryTaskStatusFailed, + ErrorCode: "ALL_PACKAGES_REJECTED", ErrorMessage: "all packages were rejected", + ResultObjectID: "task-logs/" + claimedFailure.ID + "/result.jsonl.zst", + ResultObjectSHA256: strings.Repeat("b", 64), QuarantineCount: 1} + failureRollback := errors.New("rollback remote upload failure") + if _, err = service.CompleteWithMutation(failureInput, func(tx entity.Session, _ bool) error { + if importErr := importIntegrationUploadFailure(contentRepository, snapshot, quarantine, + failureInput.ErrorCode, failureInput.ErrorMessage, tx); importErr != nil { + return importErr + } + return failureRollback + }); !errors.Is(err, failureRollback) { + t.Fatalf("CompleteWithMutation(failure rollback) error = %v", err) + } + storedQuarantine, err := contentRepository.QuarantineByTask(claimedFailure.ID, nil) + if err != nil || len(storedQuarantine) != 0 { + t.Fatalf("rolled back quarantine count/error = %d/%v", len(storedQuarantine), err) + } + storedUpload, err := contentRepository.GetUpload(upload.ID, false, nil) + if err != nil || storedUpload.Status != request.RepositoryUploadStatusReady { + t.Fatalf("upload state after rollback = %#v/%v", storedUpload, err) + } + failedTask, err := service.CompleteWithMutation(failureInput, func(tx entity.Session, _ bool) error { + return importIntegrationUploadFailure(contentRepository, snapshot, quarantine, + failureInput.ErrorCode, failureInput.ErrorMessage, tx) + }) + if err != nil || failedTask.Status != request.RepositoryTaskStatusFailed { + t.Fatalf("CompleteWithMutation(failure) task/error = %#v/%v", failedTask, err) + } + failedTaskAgain, err := service.CompleteWithMutation(failureInput, func(tx entity.Session, _ bool) error { + return importIntegrationUploadFailure(contentRepository, snapshot, quarantine, + failureInput.ErrorCode, failureInput.ErrorMessage, tx) + }) + if err != nil || failedTaskAgain.Status != request.RepositoryTaskStatusFailed { + t.Fatalf("CompleteWithMutation(failure idempotent) task/error = %#v/%v", failedTaskAgain, err) + } + storedQuarantine, err = contentRepository.QuarantineByTask(claimedFailure.ID, nil) + storedUpload, uploadErr := contentRepository.GetUpload(upload.ID, false, nil) + activeAfterFailure, repositoryErr := repositories.GetByID(integrationRepoTwo, nil) + if err != nil || uploadErr != nil || repositoryErr != nil || len(storedQuarantine) != 1 || + storedUpload.Status != request.RepositoryUploadStatusFailed || + activeAfterFailure.ActiveRevisionID != integrationRevisionAtomic { + t.Fatalf("persisted failed upload/quarantine/active = %#v/%d/%#v errors=%v/%v/%v", + storedUpload, len(storedQuarantine), activeAfterFailure, uploadErr, err, repositoryErr) + } + t.Logf("failed upload transaction: status=%s quarantine=%d active_revision=%s", + storedUpload.Status, len(storedQuarantine), activeAfterFailure.ActiveRevisionID) + + taskD := enqueueIntegrationTask(t, service, ctx, integrationRepoOne, request.RepositoryTaskTypeSync, "", 2) + taskE := enqueueIntegrationTask(t, service, ctx, integrationRepoOne, request.RepositoryTaskTypeSync, "", 2) + claimedD, err := service.ClaimNext("worker-d", 10*time.Second) + assertClaim(t, claimedD, err, taskD.ID, true) + now = now.Add(11 * time.Second) + claimedE, err := service.ClaimNext("worker-e", 10*time.Second) + assertClaim(t, claimedE, err, taskE.ID, true) + if claimedE.FencingToken <= claimedD.FencingToken { + t.Fatalf("stolen lock token did not increase: old=%d new=%d", claimedD.FencingToken, claimedE.FencingToken) + } + t.Logf("fencing takeover: stale_token=%d new_token=%d", claimedD.FencingToken, claimedE.FencingToken) + if _, err = service.Complete(request.CompleteRepositoryTaskRequest{ + TaskID: taskD.ID, WorkerID: "worker-d", FencingToken: claimedD.FencingToken, + Status: request.RepositoryTaskStatusSuccess, + }); !errors.Is(err, ErrTaskLeaseExpired) { + t.Fatalf("Complete(stale worker) error = %v", err) + } + + restarted := newService(db, repositoryrepo.New(db), repositorytaskrepo.New(db), repositorylockrepo.New(db), + infrarepo.NewInfrastructureRepository(db)) + restarted.now = func() time.Time { return now } + if recovered, err := restarted.RecoverExpired(); err != nil || recovered != 1 { + t.Fatalf("RecoverExpired() recovered/error = %d/%v", recovered, err) + } + requeuedD, err := restarted.Describe(ctx, taskD.ID) + if err != nil || requeuedD.Status != request.RepositoryTaskStatusPending || + requeuedD.ErrorCode != "LEASE_EXPIRED_RETRY" { + t.Fatalf("recovered task = %#v, error = %v", requeuedD, err) + } + completeIntegrationTask(t, restarted, claimedE, "worker-e", request.RepositoryTaskStatusSuccess) + claimedD2, err := restarted.ClaimNext("worker-d2", 10*time.Second) + assertClaim(t, claimedD2, err, taskD.ID, true) + if claimedD2.Attempt != 2 || claimedD2.FencingToken <= claimedE.FencingToken { + t.Fatalf("reclaimed task attempt/token = %d/%d", claimedD2.Attempt, claimedD2.FencingToken) + } + now = now.Add(11 * time.Second) + if recovered, err := restarted.RecoverExpired(); err != nil || recovered != 1 { + t.Fatalf("RecoverExpired(exhausted) recovered/error = %d/%v", recovered, err) + } + failedD, err := restarted.Describe(ctx, taskD.ID) + if err != nil || failedD.Status != request.RepositoryTaskStatusFailed || failedD.ErrorCode != "LEASE_EXPIRED" { + t.Fatalf("exhausted task = %#v, error = %v", failedD, err) + } + t.Logf("lease recovery: first_status=%s exhausted_status=%s attempts=%d", + requeuedD.Status, failedD.Status, failedD.Attempt) + retryD, err := restarted.Retry(ctx, taskD.ID) + if err != nil || retryD.RootTaskID != taskD.RootTaskID || retryD.RetryOfTaskID != taskD.ID { + t.Fatalf("Retry() task/error = %#v/%v", retryD, err) + } + repeatedRetry, err := restarted.Retry(ctx, taskD.ID) + if err != nil || repeatedRetry.ID != retryD.ID { + t.Fatalf("Retry(pending child) task/error = %#v/%v", repeatedRetry, err) + } + canceledRetry, err := restarted.Cancel(ctx, retryD.ID) + if err != nil || canceledRetry.Status != request.RepositoryTaskStatusCanceled { + t.Fatalf("Cancel(pending) task/error = %#v/%v", canceledRetry, err) + } + + // Once the first child is terminal another retry is allowed. Concurrent + // callers still share one child because Retry locks the original task row. + const retryCallers = 8 + retryIDs := make(chan string, retryCallers) + retryErrors := make(chan error, retryCallers) + var retryWG sync.WaitGroup + for index := 0; index < retryCallers; index++ { + retryWG.Add(1) + go func() { + defer retryWG.Done() + child, retryErr := restarted.Retry(ctx, taskD.ID) + if retryErr != nil { + retryErrors <- retryErr + return + } + retryIDs <- child.ID + }() + } + retryWG.Wait() + close(retryIDs) + close(retryErrors) + for retryErr := range retryErrors { + t.Errorf("concurrent Retry() error = %v", retryErr) + } + var sharedRetryID string + for id := range retryIDs { + if sharedRetryID == "" { + sharedRetryID = id + } + if id != sharedRetryID || id == retryD.ID { + t.Fatalf("concurrent Retry() IDs differ or reused terminal child: %s/%s", id, sharedRetryID) + } + } + if sharedRetryID == "" || t.Failed() { + t.Fatal("concurrent Retry() did not return one successful child") + } + var activeRetries int64 + if err = db.DB().Model(&po.RepositoryTask{}). + Where("retry_of_task_id = ? AND status IN ?", taskD.ID, []string{ + request.RepositoryTaskStatusPending, request.RepositoryTaskStatusRunning, + request.RepositoryTaskStatusCancelRequested, + }).Count(&activeRetries).Error; err != nil || activeRetries != 1 { + t.Fatalf("active retry count/error = %d/%v", activeRetries, err) + } + claimedRetry, err := restarted.ClaimNext("worker-retry", 10*time.Second) + assertClaim(t, claimedRetry, err, sharedRetryID, true) + runningRetry, err := restarted.Retry(ctx, taskD.ID) + if err != nil || runningRetry.ID != sharedRetryID || runningRetry.Status != request.RepositoryTaskStatusRunning { + t.Fatalf("Retry(running child) task/error = %#v/%v", runningRetry, err) + } + if _, err = restarted.Cancel(ctx, sharedRetryID); err != nil { + t.Fatal(err) + } + cancelingRetry, err := restarted.Retry(ctx, taskD.ID) + if err != nil || cancelingRetry.ID != sharedRetryID || + cancelingRetry.Status != request.RepositoryTaskStatusCancelRequested { + t.Fatalf("Retry(canceling child) task/error = %#v/%v", cancelingRetry, err) + } + completeIntegrationTask(t, restarted, claimedRetry, "worker-retry", request.RepositoryTaskStatusCanceled) + t.Logf("retry deduplication: callers=%d active_children=%d child=%s", retryCallers, activeRetries, sharedRetryID) + + taskF := enqueueIntegrationTask(t, restarted, ctx, integrationRepoTwo, request.RepositoryTaskTypeSync, "", 2) + claimedF, err := restarted.ClaimNext("worker-f", 10*time.Second) + assertClaim(t, claimedF, err, taskF.ID, true) + cancelRequested, err := restarted.Cancel(ctx, taskF.ID) + if err != nil || cancelRequested.Status != request.RepositoryTaskStatusCancelRequested { + t.Fatalf("Cancel(running) task/error = %#v/%v", cancelRequested, err) + } + if _, err = restarted.Complete(request.CompleteRepositoryTaskRequest{ + TaskID: taskF.ID, WorkerID: "worker-f", FencingToken: claimedF.FencingToken, + Status: request.RepositoryTaskStatusSuccess, + }); !errors.Is(err, ErrInvalidTaskTransition) { + t.Fatalf("Complete(cancel requested as success) error = %v", err) + } + mutationCalled = false + if err = restarted.ExecuteFencedMutation(request.ExecuteRepositoryMutationRequest{ + TaskID: claimedF.ID, WorkerID: "worker-f", FencingToken: claimedF.FencingToken, + RevisionID: integrationRevision, + }, func(entity.Session) error { + mutationCalled = true + return nil + }); !errors.Is(err, ErrInvalidTaskTransition) || mutationCalled { + t.Fatalf("ExecuteFencedMutation(canceled) called/error = %v/%v", mutationCalled, err) + } + completeIntegrationTask(t, restarted, claimedF, "worker-f", request.RepositoryTaskStatusCanceled) + + taskSync := enqueueIntegrationTask(t, restarted, ctx, integrationRepoTwo, request.RepositoryTaskTypeSync, "", 2) + claimedSync, err := restarted.ClaimNext("worker-sync", 10*time.Second) + assertClaim(t, claimedSync, err, taskSync.ID, true) + if _, err = restarted.Complete(request.CompleteRepositoryTaskRequest{ + TaskID: taskSync.ID, WorkerID: "worker-sync", FencingToken: claimedSync.FencingToken, + Status: request.RepositoryTaskStatusPartialSuccess, + }); !errors.Is(err, ErrInvalidTaskTransition) { + t.Fatalf("Complete(sync partial success) error = %v", err) + } + completeIntegrationTask(t, restarted, claimedSync, "worker-sync", request.RepositoryTaskStatusFailed) + + crossRepositoryOne := enqueueIntegrationTask(t, restarted, ctx, integrationRepoOne, + request.RepositoryTaskTypeSync, "", 2) + crossRepositoryTwo := enqueueIntegrationTask(t, restarted, ctx, integrationRepoTwo, + request.RepositoryTaskTypeSync, "", 2) + crossClaims := claimConcurrently(t, restarted, []string{"worker-cross-1", "worker-cross-2"}) + if len(crossClaims) != 2 { + t.Fatalf("concurrent cross-repository claims = %d, want 2", len(crossClaims)) + } + t.Logf("concurrent cross-repository claims=%d", len(crossClaims)) + crossIDs := map[string]bool{crossRepositoryOne.ID: false, crossRepositoryTwo.ID: false} + for _, value := range crossClaims { + crossIDs[value.ID] = true + completeIntegrationTask(t, restarted, value, value.WorkerID, request.RepositoryTaskStatusSuccess) + } + if !crossIDs[crossRepositoryOne.ID] || !crossIDs[crossRepositoryTwo.ID] { + t.Fatalf("cross-repository claimed IDs = %#v", crossIDs) + } + + concurrentOne := enqueueIntegrationTask(t, restarted, ctx, integrationRepoOne, + request.RepositoryTaskTypeSync, "", 2) + concurrentTwo := enqueueIntegrationTask(t, restarted, ctx, integrationRepoOne, + request.RepositoryTaskTypeUpload, "", 2) + sameRepositoryClaims := claimConcurrently(t, restarted, []string{"worker-race-1", "worker-race-2"}) + if len(sameRepositoryClaims) != 1 || + (sameRepositoryClaims[0].ID != concurrentOne.ID && sameRepositoryClaims[0].ID != concurrentTwo.ID) { + t.Fatalf("concurrent same-repository claims = %#v", sameRepositoryClaims) + } + t.Logf("concurrent same-repository mutation claims=%d", len(sameRepositoryClaims)) + completeIntegrationTask(t, restarted, sameRepositoryClaims[0], sameRepositoryClaims[0].WorkerID, + request.RepositoryTaskStatusSuccess) + + diagnosisTask := enqueueIntegrationTask(t, restarted, ctx, integrationRepoTwo, + request.RepositoryTaskTypeValidate, integrationRevision, 2) + var diagnosisClaim *po.RepositoryTask + for attempt := 0; attempt < 4; attempt++ { + candidate, claimErr := restarted.ClaimNext("worker-diagnosis", 10*time.Second) + if claimErr != nil || candidate == nil { + t.Fatalf("ClaimNext(diagnosis) task/error = %#v/%v", candidate, claimErr) + } + if candidate.ID == diagnosisTask.ID { + diagnosisClaim = candidate + break + } + completeIntegrationTask(t, restarted, candidate, "worker-diagnosis", request.RepositoryTaskStatusSuccess) + } + if diagnosisClaim == nil { + t.Fatal("diagnosis validation task was not claimed") + } + if _, err = restarted.Heartbeat(request.HeartbeatRepositoryTaskRequest{ + TaskID: diagnosisClaim.ID, StorageNodeID: integrationNodeTwo, + WorkerID: diagnosisClaim.WorkerID, FencingToken: diagnosisClaim.FencingToken, + LeaseSeconds: 10, ProgressPercent: 80, + CurrentStep: "resolve package dependencies token=hidden-step", + }); err != nil { + t.Fatalf("Heartbeat(diagnosis) error = %v", err) + } + completedDiagnosis, err := restarted.Complete(request.CompleteRepositoryTaskRequest{ + TaskID: diagnosisClaim.ID, WorkerID: diagnosisClaim.WorkerID, FencingToken: diagnosisClaim.FencingToken, + Status: request.RepositoryTaskStatusFailed, ErrorCode: "DNF_VALIDATION_FAILED", + ErrorMessage: "nothing provides ocm-sync-dependency; token=do-not-export https://user:pass@repo.invalid", + }) + if err != nil { + t.Fatalf("Complete(diagnosis) error = %v", err) + } + diagnosis := completedDiagnosis.StructuredDiagnosis() + if diagnosis == nil || diagnosis.RuleID != "DNF_DEPENDENCY_MISSING" || + diagnosis.FailedStep != "resolve package dependencies" { + t.Fatalf("completed diagnosis = %#v", diagnosis) + } + if strings.Contains(completedDiagnosis.ErrorMessage, "do-not-export") || + strings.Contains(completedDiagnosis.ErrorMessage, "user:pass") { + t.Fatalf("stored task error was not sanitized: %s", completedDiagnosis.ErrorMessage) + } + if strings.Contains(completedDiagnosis.CurrentStep, "hidden-step") { + t.Fatalf("stored task step was not sanitized: %s", completedDiagnosis.CurrentStep) + } + exported, err := restarted.ExportDiagnosis(ctx, diagnosisTask.ID) + if err != nil { + t.Fatalf("ExportDiagnosis() error = %v", err) + } + digest := sha256.Sum256([]byte(exported.Content)) + if exported.ContentType != "application/json" || exported.SHA256 != hex.EncodeToString(digest[:]) || + !strings.Contains(exported.Content, `"schema_version": "ocmanager.repository.diagnosis/v1"`) || + !strings.Contains(exported.Content, `"rule_id": "DNF_DEPENDENCY_MISSING"`) || + strings.Contains(exported.Content, "do-not-export") || strings.Contains(exported.Content, "user:pass") || + strings.Contains(exported.Content, "hidden-step") { + t.Fatalf("ExportDiagnosis() = %#v", exported) + } + t.Logf("diagnosis persisted/exported: rule=%s category=%s sha256=%s", + diagnosis.RuleID, diagnosis.FailureCategory, exported.SHA256) + + values, page, err := restarted.List(ctx, request.ListRepositoryTasksRequest{ + RepositoryID: integrationRepoOne, + PageRequest: request.PageRequest{PageNumber: 1, PageSize: 100}, + }) + if err != nil || len(values) == 0 || page.Count == 0 { + t.Fatalf("List() values/page/error = %d/%#v/%v", len(values), page, err) + } + idempotencyKey := "repository-schedule-run:2026-09-20T03:00:00Z" + idempotentInput := request.EnqueueRepositoryTaskRequest{ + RepositoryID: integrationRepoTwo, RevisionID: integrationRevision, + Type: request.RepositoryTaskTypeValidate, PayloadJSON: `{"fixture":"idempotency"}`, + MaxAttempts: 2, Trigger: request.RepositoryTaskTriggerScheduled, IdempotencyKey: idempotencyKey, + } + firstIdempotent, err := restarted.Enqueue(ctx, idempotentInput) + if err != nil { + t.Fatalf("first idempotent Enqueue() error = %v", err) + } + secondIdempotent, err := restarted.Enqueue(ctx, idempotentInput) + if err != nil || secondIdempotent.ID != firstIdempotent.ID || + secondIdempotent.IdempotencyKey == nil || *secondIdempotent.IdempotencyKey != idempotencyKey { + t.Fatalf("idempotent Enqueue() tasks/error = %#v/%#v/%v", firstIdempotent, secondIdempotent, err) + } + + if _, err = restarted.Enqueue(ctx, request.EnqueueRepositoryTaskRequest{ + RepositoryID: integrationRepoMigration, Type: request.RepositoryTaskTypeSync, + }); !errors.Is(err, ErrRepositoryMigrationRequired) { + t.Fatalf("unassigned repository accepted a normal task: %v", err) + } + migration, err := restarted.EnqueueMigrationInSession(ctx, request.EnqueueRepositoryTaskRequest{ + RepositoryID: integrationRepoMigration, RevisionID: integrationRevision, + Type: request.RepositoryTaskTypeMigrateRepository, PayloadJSON: `{"source":"legacy"}`, + }, integrationNodeOne, nil) + if err != nil || migration.StorageNodeID != integrationNodeOne || + migration.Type != request.RepositoryTaskTypeMigrateRepository || !migration.RequiresMutationLock { + t.Fatalf("migration task routing = %#v/%v", migration, err) + } + t.Logf("migration routing: repository=%s target_node=%s task=%s", + integrationRepoMigration, integrationNodeOne, migration.ID) +} + +func importIntegrationUploadFailure(content repositorycontentrepo.Repository, + snapshot request.RepositoryUploadSnapshot, quarantine []po.RepositoryQuarantineItem, + errorCode, errorMessage string, session entity.Session, +) error { + stored, err := content.QuarantineByTask(quarantine[0].TaskID, session) + if err != nil { + return err + } + if len(stored) == 0 { + if err = content.CreateQuarantine(&quarantine[0], session); err != nil { + return err + } + } + upload, err := content.GetUpload(snapshot.UploadID, true, session) + if err != nil { + return err + } + upload.Status = request.RepositoryUploadStatusFailed + upload.ErrorCode = errorCode + upload.ErrorMessage = errorMessage + return content.UpdateUpload(upload, session) +} + +func claimConcurrently(t *testing.T, service Service, workers []string) []*po.RepositoryTask { + t.Helper() + claims := make(chan *po.RepositoryTask, len(workers)) + errs := make(chan error, len(workers)) + start := make(chan struct{}) + var wg sync.WaitGroup + for _, worker := range workers { + wg.Add(1) + go func(workerID string) { + defer wg.Done() + <-start + value, claimErr := service.ClaimNext(workerID, 10*time.Second) + claims <- value + errs <- claimErr + }(worker) + } + close(start) + wg.Wait() + close(claims) + close(errs) + for claimErr := range errs { + if claimErr != nil { + t.Fatalf("concurrent ClaimNext() error = %v", claimErr) + } + } + values := make([]*po.RepositoryTask, 0, len(workers)) + for value := range claims { + if value != nil { + values = append(values, value) + } + } + return values +} + +func enqueueIntegrationTask(t *testing.T, service Service, ctx entity.Context, repositoryID, taskType, + revisionID string, maxAttempts int, +) *po.RepositoryTask { + t.Helper() + value, err := service.Enqueue(ctx, request.EnqueueRepositoryTaskRequest{ + RepositoryID: repositoryID, + RevisionID: revisionID, + Type: taskType, + PayloadJSON: `{"fixture":true}`, + MaxAttempts: maxAttempts, + }) + if err != nil { + t.Fatalf("Enqueue(%s) error = %v", taskType, err) + } + return value +} + +func assertClaim(t *testing.T, value *po.RepositoryTask, err error, expectedID string, mutation bool) { + t.Helper() + if err != nil || value == nil || value.ID != expectedID || + value.Status != request.RepositoryTaskStatusRunning || value.Attempt < 1 { + t.Fatalf("ClaimNext() value/error = %#v/%v, expected ID %s", value, err, expectedID) + } + if mutation && value.FencingToken == 0 { + t.Fatal("mutation task fencing token = 0") + } +} + +func completeIntegrationTask(t *testing.T, service Service, value *po.RepositoryTask, workerID, status string) { + t.Helper() + completed, err := service.Complete(request.CompleteRepositoryTaskRequest{ + TaskID: value.ID, + WorkerID: workerID, + FencingToken: value.FencingToken, + Status: status, + }) + if err != nil || completed.Status != status { + t.Fatalf("Complete(%s) task/error = %#v/%v", status, completed, err) + } +} + +func createIntegrationRepository(t *testing.T, db *database.DBClient, id, name, storageNodeID string) { + t.Helper() + value := &po.SoftwareRepository{ + ID: id, + ProjectID: integrationProjectID, + SystemProduct: "OpenCloudOS", + ProductVersion: "9.6", + Name: name, + Architecture: request.RepositoryArchitectureX8664, + Type: request.RepositoryTypePrivate, + StorageLocation: "public/" + name, + PublishKey: "phase2-" + name, + PrimaryStorageNodeID: storageNodeID, + PublishBaseURLSnapshot: "http://repository-node.invalid:18090", + AllowUpload: true, + IncludePackagesJSON: "[]", + ExcludePackagesJSON: "[]", + GPGPolicy: request.RepositoryGPGDisabled, + Enabled: true, + Status: request.RepositoryStatusEmpty, + Creator: "repository-admin", + } + if err := db.DB().Create(value).Error; err != nil { + t.Fatalf("create repository %s: %v", id, err) + } +} + +func taskIntegrationContext() entity.Context { + ctx := entity.NewDefaultContext() + ctx.User = &ftpb.UserInfo{LoginName: "repository-admin"} + return ctx +} + +func integrationRevisionFixture(task *po.RepositoryTask) *po.RepositoryRevision { + now := time.Date(2026, 8, 30, 15, 0, 0, 0, time.UTC) + return &po.RepositoryRevision{ID: integrationRevisionAtomic, RepositoryID: task.RepositoryID, + StorageNodeID: task.StorageNodeID, TaskID: task.ID, Kind: request.RepositoryRevisionKindSync, + Status: request.RepositoryRevisionStatusPublished, ManifestSHA256: strings.Repeat("f", 64), + RepomdSHA256: strings.Repeat("a", 64), SnapshotManifestSHA256: strings.Repeat("d", 64), + RelativePath: "public/atomic/revisions/" + integrationRevisionAtomic, + CreatedBy: "repository-admin", PublishedAt: &now} +} + +func cleanupTaskIntegrationRows(db *database.DBClient) { + migrator := db.DB().Migrator() + if migrator.HasTable(&po.RepositoryQuarantineItem{}) { + db.DB().Where("repository_id IN ?", []string{integrationRepoOne, integrationRepoTwo, integrationRepoMigration}). + Delete(&po.RepositoryQuarantineItem{}) + } + if migrator.HasTable(&po.RepositoryUpload{}) { + db.DB().Where("project_id = ?", integrationProjectID).Delete(&po.RepositoryUpload{}) + } + if migrator.HasTable(&po.RepositoryPackage{}) { + db.DB().Where("repository_id IN ?", []string{integrationRepoOne, integrationRepoTwo, integrationRepoMigration}). + Delete(&po.RepositoryPackage{}) + } + if migrator.HasTable(&po.RepositoryRevision{}) { + db.DB().Where("repository_id IN ?", []string{integrationRepoOne, integrationRepoTwo, integrationRepoMigration}). + Delete(&po.RepositoryRevision{}) + } + if migrator.HasTable(&po.RepositoryTask{}) && migrator.HasColumn(&po.RepositoryTask{}, "project_id") { + db.DB().Where("project_id = ?", integrationProjectID).Delete(&po.RepositoryTask{}) + } + if migrator.HasTable(&po.RepositoryMutationLock{}) { + db.DB().Where("repository_id IN ?", []string{integrationRepoOne, integrationRepoTwo, integrationRepoMigration}). + Delete(&po.RepositoryMutationLock{}) + } + if migrator.HasTable(&po.SoftwareRepository{}) { + db.DB().Where("project_id = ?", integrationProjectID).Delete(&po.SoftwareRepository{}) + } + if migrator.HasTable(&po.Project{}) { + db.DB().Where("id = ?", integrationProjectID).Delete(&po.Project{}) + } +} + +func cleanupPreflightIntegrationRows(db *database.DBClient) { + migrator := db.DB().Migrator() + if migrator.HasTable(&po.RepositoryMutationLock{}) { + db.DB().Where("repository_id = ?", preflightRepositoryID).Delete(&po.RepositoryMutationLock{}) + } + if migrator.HasTable(&po.RepositoryTask{}) { + db.DB().Where("project_id = ?", preflightProjectID).Delete(&po.RepositoryTask{}) + } + if migrator.HasTable(&po.RepositoryPreflightRecord{}) { + db.DB().Where("repository_id = ? OR storage_node_id = ?", preflightRepositoryID, preflightNodeID). + Delete(&po.RepositoryPreflightRecord{}) + } + if migrator.HasTable(&po.SoftwareRepository{}) { + db.DB().Where("id = ?", preflightRepositoryID).Delete(&po.SoftwareRepository{}) + } + if migrator.HasTable(&po.RepositoryStorageNode{}) { + db.DB().Where("id = ?", preflightNodeID).Delete(&po.RepositoryStorageNode{}) + } + if migrator.HasTable(&po.Project{}) { + db.DB().Where("id = ?", preflightProjectID).Delete(&po.Project{}) + } +} diff --git a/manager/backend/services/footstone/domain/repositoryTaskService/service_test.go b/manager/backend/services/footstone/domain/repositoryTaskService/service_test.go new file mode 100644 index 0000000000000000000000000000000000000000..3c57ed90246c3c81ca51eaed0edb7baa661e58a9 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryTaskService/service_test.go @@ -0,0 +1,206 @@ +package repositoryTaskService + +import ( + "errors" + "strings" + "testing" + "time" + + "gorm.io/gorm" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +const testRepositoryID = "11111111-1111-1111-1111-111111111111" + +func TestNormalizeEnqueueRequest(t *testing.T) { + got, err := normalizeEnqueueRequest(request.EnqueueRepositoryTaskRequest{ + RepositoryID: testRepositoryID, + Type: " sync ", + }) + if err != nil { + t.Fatal(err) + } + if got.Type != request.RepositoryTaskTypeSync || got.PayloadJSON != "{}" || + got.MaxAttempts != defaultMaxAttempts || got.Trigger != request.RepositoryTaskTriggerManual { + t.Fatalf("normalizeEnqueueRequest() = %#v", got) + } +} + +func TestNormalizeTaskTriggerAcceptsSystemForEnqueueAndFilter(t *testing.T) { + got, err := normalizeEnqueueRequest(request.EnqueueRepositoryTaskRequest{ + RepositoryID: testRepositoryID, + Type: request.RepositoryTaskTypeSync, + Trigger: " system ", + }) + if err != nil || got.Trigger != request.RepositoryTaskTriggerSystem { + t.Fatalf("normalize system enqueue trigger = %q/%v", got.Trigger, err) + } + filter, err := normalizeTaskTrigger(" system ", false) + if err != nil || filter != request.RepositoryTaskTriggerSystem { + t.Fatalf("normalize system filter trigger = %q/%v", filter, err) + } +} + +func TestNormalizeEnqueueRequestRequiresRevisionForReadOnlyTasks(t *testing.T) { + for _, taskType := range []string{request.RepositoryTaskTypeValidate, request.RepositoryTaskTypeDistribute} { + _, err := normalizeEnqueueRequest(request.EnqueueRepositoryTaskRequest{ + RepositoryID: testRepositoryID, + Type: taskType, + }) + if err == nil { + t.Fatalf("normalizeEnqueueRequest(%s) error = nil", taskType) + } + } +} + +func TestNormalizeEnqueueRequestRejectsInvalidPayloadAndAttempts(t *testing.T) { + tests := []request.EnqueueRepositoryTaskRequest{ + {RepositoryID: testRepositoryID, Type: request.RepositoryTaskTypeSync, PayloadJSON: "{"}, + {RepositoryID: testRepositoryID, Type: request.RepositoryTaskTypeSync, MaxAttempts: 11}, + {RepositoryID: testRepositoryID, Type: "UNKNOWN"}, + {RepositoryID: testRepositoryID, Type: request.RepositoryTaskTypeSync, Trigger: "UNKNOWN"}, + {RepositoryID: testRepositoryID, Type: request.RepositoryTaskTypeSync, IdempotencyKey: strings.Repeat("x", 129)}, + {RepositoryID: testRepositoryID, Type: request.RepositoryTaskTypeSync, BandwidthLimitBytes: -1}, + {RepositoryID: testRepositoryID, Type: request.RepositoryTaskTypeSync, MaxParallelDownloads: 21}, + } + for _, input := range tests { + if _, err := normalizeEnqueueRequest(input); err == nil { + t.Fatalf("normalizeEnqueueRequest(%#v) error = nil", input) + } + } +} + +func TestMutationLockClassification(t *testing.T) { + if !requiresMutationLock(request.RepositoryTaskTypeSync) || + !requiresMutationLock(request.RepositoryTaskTypeMigrateRepository) || + requiresMutationLock(request.RepositoryTaskTypeValidate) || + requiresMutationLock(request.RepositoryTaskTypeDistribute) { + t.Fatal("requiresMutationLock() classification is incorrect") + } +} + +func TestPreflightErrorDetailUsesStableCode(t *testing.T) { + code, message := preflightErrorDetail(testCodedError{}) + if code != "REPOSITORY_NODE_INSUFFICIENT_SPACE" || message != "capacity gate failed" { + t.Fatalf("preflightErrorDetail() = %s/%s", code, message) + } +} + +type testCodedError struct{} + +func (testCodedError) Error() string { + return "REPOSITORY_NODE_INSUFFICIENT_SPACE: capacity gate failed" +} +func (testCodedError) Code() string { return "REPOSITORY_NODE_INSUFFICIENT_SPACE" } + +func TestNormalizeEnqueueRequestAcceptsMigration(t *testing.T) { + got, err := normalizeEnqueueRequest(request.EnqueueRepositoryTaskRequest{ + RepositoryID: testRepositoryID, + RevisionID: "22222222-2222-4222-8222-222222222222", + Type: request.RepositoryTaskTypeMigrateRepository, + PayloadJSON: `{ "target_storage_node_id": "33333333-3333-4333-8333-333333333333" }`, + }) + if err != nil || got.Type != request.RepositoryTaskTypeMigrateRepository || got.RevisionID == "" { + t.Fatalf("normalize migration task = %#v/%v", got, err) + } +} + +func TestVerifyOwnership(t *testing.T) { + now := time.Now().UTC() + lease := now.Add(time.Minute) + value := &po.RepositoryTask{ + Status: request.RepositoryTaskStatusRunning, + WorkerID: "worker-1", + FencingToken: 9, + LeaseExpiresAt: &lease, + } + if err := verifyOwnership(value, "worker-1", 9, now); err != nil { + t.Fatalf("verifyOwnership() error = %v", err) + } + if err := verifyOwnership(value, "worker-2", 9, now); !errors.Is(err, ErrTaskOwnershipLost) { + t.Fatalf("verifyOwnership(wrong worker) error = %v", err) + } + if err := verifyOwnership(value, "worker-1", 8, now); !errors.Is(err, ErrTaskOwnershipLost) { + t.Fatalf("verifyOwnership(stale token) error = %v", err) + } + if err := verifyOwnership(value, "worker-1", 9, lease); !errors.Is(err, ErrTaskLeaseExpired) { + t.Fatalf("verifyOwnership(expired lease) error = %v", err) + } +} + +func TestClearPreparedCommit(t *testing.T) { + now := time.Now().UTC() + value := &po.RepositoryTask{ + CommitState: request.RepositoryTaskCommitStatePrepared, CommitToken: "token", + PreparedRevisionID: "revision", PreparedManifestSHA256: "manifest", + ResultObjectID: "result", ResultObjectSHA256: "result-sha", + PreparedCompletionStatus: request.RepositoryTaskStatusPartialSuccess, + PreparedErrorCode: "SOME_PACKAGES_REJECTED", PreparedErrorMessage: "one package rejected", + PreparedAt: &now, ActivatedAt: &now, ConfirmedAt: &now, + } + clearPreparedCommit(value) + if value.CommitState != request.RepositoryTaskCommitStateNone || value.CommitToken != "" || + value.PreparedRevisionID != "" || value.PreparedManifestSHA256 != "" || + value.ResultObjectID != "" || value.ResultObjectSHA256 != "" || + value.PreparedCompletionStatus != request.RepositoryTaskStatusSuccess || + value.PreparedErrorCode != "" || value.PreparedErrorMessage != "" || + value.PreparedAt != nil || value.ActivatedAt != nil || value.ConfirmedAt != nil { + t.Fatalf("clearPreparedCommit() left prepared state: %#v", value) + } +} + +func TestRecordPackageDeleteResultsPreservesPerPackageOutcome(t *testing.T) { + repository := &captureTaskResultRepository{} + service := &service{results: repository, now: func() time.Time { + return time.Date(2026, 9, 30, 9, 0, 0, 0, time.UTC) + }} + task := &po.RepositoryTask{ID: "22222222-2222-4222-8222-222222222222", + Type: request.RepositoryTaskTypeDeletePackage} + session := &gorm.DB{} + if err := service.RecordPackageDeleteResults(task, + []string{"pkg-b-0:1-1.noarch", "pkg-a-0:1-1.noarch"}, session); err != nil { + t.Fatal(err) + } + if len(repository.items) != 2 || repository.items[0].Status != request.RepositoryTaskStatusSuccess || + repository.items[0].ErrorCode != "" || !strings.Contains(string(repository.items[0].ResultJSON), `"deleted":true`) { + t.Fatalf("success result items = %#v", repository.items) + } + if err := service.recordPackageDeleteResults(task, []string{"pkg-a-0:1-1.noarch"}, + request.RepositoryTaskStatusFailed, "REVISION_BUILD_FAILED", "createrepo_c failed", session); err != nil { + t.Fatal(err) + } + failed := repository.items[len(repository.items)-1] + if failed.Status != request.RepositoryTaskStatusFailed || failed.ErrorCode != "REVISION_BUILD_FAILED" || + failed.Message != "createrepo_c failed" || !strings.Contains(string(failed.ResultJSON), `"deleted":false`) { + t.Fatalf("failed result item = %#v", failed) + } +} + +func TestDeletePackageTaskNEVRAsSupportsBatchAndLegacyPayload(t *testing.T) { + batch, err := deletePackageTaskNEVRAs(`{"nevras":["a","b"]}`) + if err != nil || strings.Join(batch, ",") != "a,b" { + t.Fatalf("batch NEVRAs = %#v/%v", batch, err) + } + legacy, err := deletePackageTaskNEVRAs(`{"nevra":"legacy"}`) + if err != nil || len(legacy) != 1 || legacy[0] != "legacy" { + t.Fatalf("legacy NEVRAs = %#v/%v", legacy, err) + } +} + +type captureTaskResultRepository struct { + items []po.RepositoryTaskResultItem +} + +func (*captureTaskResultRepository) SyncTables() error { return nil } + +func (r *captureTaskResultRepository) CreateItems(values []po.RepositoryTaskResultItem, _ entity.Session) error { + r.items = append(r.items, values...) + return nil +} + +func (r *captureTaskResultRepository) ListByTaskID(string, entity.Session) ([]po.RepositoryTaskResultItem, error) { + return append([]po.RepositoryTaskResultItem(nil), r.items...), nil +} diff --git a/manager/backend/services/footstone/domain/repositoryUpstreamService/service.go b/manager/backend/services/footstone/domain/repositoryUpstreamService/service.go new file mode 100644 index 0000000000000000000000000000000000000000..49df142322d0354712ea9eb470d66ad67c673725 --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryUpstreamService/service.go @@ -0,0 +1,323 @@ +package repositoryUpstreamService + +import ( + "errors" + "fmt" + "net/url" + "path/filepath" + "regexp" + "strings" + "unicode" + "unicode/utf8" + + "github.com/google/uuid" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/infrarepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositoryupstreamprofilerepo" +) + +var ( + ErrUnauthenticated = errors.New("authenticated user is required") + ErrForbidden = errors.New("user cannot manage repository upstream profiles in this project") + ErrProjectChange = errors.New("repository upstream profile project cannot be changed") + placeholderPattern = regexp.MustCompile(`\{[^{}]+\}`) +) + +type projectReader interface { + DescribeProject(entity.Context, request.DescribeProjectRequest, entity.Session) (request.DescribeProjectResponse, error) +} + +type Service = *service + +type service struct { + db entity.SessionFactory + profiles repositoryupstreamprofilerepo.Repository + projects projectReader +} + +func New(db *database.DBClient) Service { + return newService(db, repositoryupstreamprofilerepo.New(db), infrarepo.NewInfrastructureRepository(db)) +} + +func newService(db entity.SessionFactory, profiles repositoryupstreamprofilerepo.Repository, projects projectReader) Service { + return &service{db: db, profiles: profiles, projects: projects} +} + +func (s *service) SyncTables() error { return s.profiles.SyncTables() } + +func (s *service) Create(ctx entity.Context, spec request.RepositoryUpstreamProfileSpec) ( + value *po.RepositoryUpstreamProfile, err error, +) { + spec, err = NormalizeSpec(spec) + if err != nil { + return nil, err + } + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + if err = s.authorizeProject(ctx, spec.ProjectID, tx); err != nil { + return nil, err + } + value = &po.RepositoryUpstreamProfile{ + ID: uuid.NewString(), ProjectID: spec.ProjectID, CreatedBy: ctx.User.LoginName, + } + if err = applySpec(value, spec); err != nil { + return nil, err + } + if err = s.profiles.Create(value, tx); err != nil { + return nil, err + } + return value, nil +} + +func (s *service) Update(ctx entity.Context, id string, spec request.RepositoryUpstreamProfileSpec) ( + value *po.RepositoryUpstreamProfile, err error, +) { + if err = validateID(id); err != nil { + return nil, err + } + spec, err = NormalizeSpec(spec) + if err != nil { + return nil, err + } + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + value, err = s.profiles.GetByID(id, tx) + if err != nil { + return nil, err + } + if value.ProjectID != spec.ProjectID { + return nil, ErrProjectChange + } + if err = s.authorizeProject(ctx, value.ProjectID, tx); err != nil { + return nil, err + } + if err = applySpec(value, spec); err != nil { + return nil, err + } + if err = s.profiles.Update(value, tx); err != nil { + return nil, err + } + return s.profiles.GetByID(id, tx) +} + +func (s *service) Describe(ctx entity.Context, id string) (*po.RepositoryUpstreamProfile, error) { + if err := validateID(id); err != nil { + return nil, err + } + value, err := s.profiles.GetByID(id, nil) + if err != nil { + return nil, err + } + if err = s.authorizeProject(ctx, value.ProjectID, nil); err != nil { + return nil, err + } + return value, nil +} + +func (s *service) List(ctx entity.Context, filter request.ListRepositoryUpstreamProfilesRequest) ( + []po.RepositoryUpstreamProfile, request.PageResponse, error, +) { + if filter.ProjectID <= 0 { + return nil, request.PageResponse{}, fmt.Errorf("project_id must be greater than zero") + } + if err := s.authorizeProject(ctx, filter.ProjectID, nil); err != nil { + return nil, request.PageResponse{}, err + } + filter.SystemProduct = strings.TrimSpace(filter.SystemProduct) + filter.Keyword = strings.TrimSpace(filter.Keyword) + if filter.PageRequest.PageNumber <= 0 { + filter.PageRequest.PageNumber = 1 + } + if filter.PageRequest.PageSize <= 0 { + filter.PageRequest.PageSize = 50 + } + if filter.PageRequest.PageSize > 200 { + return nil, request.PageResponse{}, fmt.Errorf("page_size cannot exceed 200") + } + return s.profiles.List(filter, nil) +} + +func (s *service) Delete(ctx entity.Context, id string) (err error) { + if err = validateID(id); err != nil { + return err + } + tx, maker := entity.NewSessionMaker(nil, s.db) + defer maker.Close(&err) + value, err := s.profiles.GetByID(id, tx) + if err != nil { + return err + } + if err = s.authorizeProject(ctx, value.ProjectID, tx); err != nil { + return err + } + return s.profiles.Delete(id, tx) +} + +func NormalizeSpec(spec request.RepositoryUpstreamProfileSpec) (request.RepositoryUpstreamProfileSpec, error) { + if spec.ProjectID <= 0 { + return spec, fmt.Errorf("project_id must be greater than zero") + } + var err error + if spec.Name, err = normalizeLabel("name", spec.Name, 128); err != nil { + return spec, err + } + if spec.SystemProduct, err = normalizeLabel("system_product", spec.SystemProduct, 128); err != nil { + return spec, err + } + if spec.Channel, err = normalizeLabel("channel", spec.Channel, 64); err != nil { + return spec, err + } + spec.Description = strings.TrimSpace(spec.Description) + if utf8.RuneCountInString(spec.Description) > 1024 || containsControl(spec.Description) { + return spec, fmt.Errorf("description is invalid or exceeds 1024 characters") + } + if spec.URLTemplate, err = normalizeURLTemplate(spec.URLTemplate); err != nil { + return spec, err + } + if spec.SupportedVersions, err = normalizeValues("supported_versions", spec.SupportedVersions, 50, 64); err != nil { + return spec, err + } + if spec.SupportedArchitectures, err = normalizeArchitectures(spec.SupportedArchitectures); err != nil { + return spec, err + } + if spec.DefaultIncludePackages, err = normalizePatterns("default_include_packages", spec.DefaultIncludePackages); err != nil { + return spec, err + } + if spec.DefaultExcludePackages, err = normalizePatterns("default_exclude_packages", spec.DefaultExcludePackages); err != nil { + return spec, err + } + return spec, nil +} + +func applySpec(value *po.RepositoryUpstreamProfile, spec request.RepositoryUpstreamProfileSpec) error { + value.Name = spec.Name + value.Description = spec.Description + value.SystemProduct = spec.SystemProduct + value.Channel = spec.Channel + value.URLTemplate = spec.URLTemplate + value.Enabled = spec.Enabled + return value.SetCollections(spec.SupportedVersions, spec.SupportedArchitectures, + spec.DefaultIncludePackages, spec.DefaultExcludePackages) +} + +func normalizeURLTemplate(value string) (string, error) { + value = strings.TrimSpace(value) + if value == "" || utf8.RuneCountInString(value) > 2048 || containsControl(value) { + return value, fmt.Errorf("url_template is required and cannot exceed 2048 characters") + } + for _, placeholder := range placeholderPattern.FindAllString(value, -1) { + if placeholder != "{version}" && placeholder != "{architecture}" { + return value, fmt.Errorf("url_template contains unsupported placeholder %q", placeholder) + } + } + resolved := strings.NewReplacer("{version}", "9.6", "{architecture}", "x86_64").Replace(value) + if strings.ContainsAny(resolved, "{}") { + return value, fmt.Errorf("url_template contains an incomplete placeholder") + } + parsed, err := url.Parse(resolved) + if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" { + return value, fmt.Errorf("url_template must resolve to an absolute HTTP(S) URL") + } + if parsed.User != nil || parsed.Fragment != "" { + return value, fmt.Errorf("url_template cannot contain credentials or a fragment") + } + return value, nil +} + +func normalizeArchitectures(values []string) ([]string, error) { + if len(values) == 0 || len(values) > 3 { + return nil, fmt.Errorf("supported_architectures must contain between 1 and 3 entries") + } + normalized := make([]string, 0, len(values)) + seen := make(map[string]struct{}, len(values)) + for _, value := range values { + value = strings.ToLower(strings.TrimSpace(value)) + if value == "arm64" { + value = request.RepositoryArchitectureARM64 + } + switch value { + case request.RepositoryArchitectureX8664, request.RepositoryArchitectureARM64, + request.RepositoryArchitectureNoarch: + default: + return nil, fmt.Errorf("unsupported repository architecture %q", value) + } + if _, exists := seen[value]; !exists { + seen[value] = struct{}{} + normalized = append(normalized, value) + } + } + return normalized, nil +} + +func normalizePatterns(field string, values []string) ([]string, error) { + result, err := normalizeValues(field, values, 100, 256) + if err != nil { + return nil, err + } + for _, pattern := range result { + if _, matchErr := filepath.Match(pattern, "probe"); matchErr != nil { + return nil, fmt.Errorf("%s contains invalid pattern %q: %w", field, pattern, matchErr) + } + } + return result, nil +} + +func normalizeValues(field string, values []string, maxItems, maxLength int) ([]string, error) { + if len(values) > maxItems { + return nil, fmt.Errorf("%s cannot contain more than %d entries", field, maxItems) + } + result := make([]string, 0, len(values)) + seen := make(map[string]struct{}, len(values)) + for _, value := range values { + value = strings.TrimSpace(value) + if value == "" || utf8.RuneCountInString(value) > maxLength || containsControl(value) || strings.ContainsAny(value, `/\\`) { + return nil, fmt.Errorf("%s contains an invalid value", field) + } + if _, exists := seen[value]; exists { + continue + } + seen[value] = struct{}{} + result = append(result, value) + } + return result, nil +} + +func normalizeLabel(field, value string, maxLength int) (string, error) { + value = strings.TrimSpace(value) + if value == "" || utf8.RuneCountInString(value) > maxLength || containsControl(value) || strings.ContainsAny(value, `/\\`) { + return value, fmt.Errorf("%s is invalid or exceeds %d characters", field, maxLength) + } + return value, nil +} + +func containsControl(value string) bool { return strings.IndexFunc(value, unicode.IsControl) >= 0 } + +func validateID(id string) error { + if _, err := uuid.Parse(strings.TrimSpace(id)); err != nil { + return fmt.Errorf("invalid upstream_profile_id: %w", err) + } + return nil +} + +func (s *service) authorizeProject(ctx entity.Context, projectID int64, session entity.Session) error { + if ctx.User == nil || strings.TrimSpace(ctx.User.LoginName) == "" { + return ErrUnauthenticated + } + project, err := s.projects.DescribeProject(ctx, request.DescribeProjectRequest{ID: projectID}, session) + if err != nil { + return fmt.Errorf("project %d not found: %w", projectID, err) + } + for _, role := range ctx.User.Roles { + if role.RoleId == request.DefaultGlobalRoleName { + return nil + } + } + if project.Project.Creator != ctx.User.LoginName { + return ErrForbidden + } + return nil +} diff --git a/manager/backend/services/footstone/domain/repositoryUpstreamService/service_test.go b/manager/backend/services/footstone/domain/repositoryUpstreamService/service_test.go new file mode 100644 index 0000000000000000000000000000000000000000..936b6cfcf00bc37fc3036cf58aee009b3d315b6c --- /dev/null +++ b/manager/backend/services/footstone/domain/repositoryUpstreamService/service_test.go @@ -0,0 +1,47 @@ +package repositoryUpstreamService + +import ( + "testing" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" +) + +func validSpec() request.RepositoryUpstreamProfileSpec { + return request.RepositoryUpstreamProfileSpec{ + ProjectID: 42, Name: "OpenCloudOS BaseOS", Description: " official base ", + SystemProduct: " OpenCloudOS ", Channel: " BaseOS ", + URLTemplate: "https://mirrors.opencloudos.org/opencloudos/{version}/BaseOS/{architecture}/os/", + SupportedVersions: []string{"9.6", "9.6"}, + SupportedArchitectures: []string{"x86_64", "arm64"}, + DefaultIncludePackages: []string{"bash", "bash"}, Enabled: true, + } +} + +func TestNormalizeSpec(t *testing.T) { + got, err := NormalizeSpec(validSpec()) + if err != nil { + t.Fatal(err) + } + if got.SystemProduct != "OpenCloudOS" || got.Channel != "BaseOS" || len(got.SupportedVersions) != 1 { + t.Fatalf("NormalizeSpec() = %#v", got) + } + if got.SupportedArchitectures[1] != request.RepositoryArchitectureARM64 || len(got.DefaultIncludePackages) != 1 { + t.Fatalf("NormalizeSpec() collections = %#v", got) + } +} + +func TestNormalizeSpecRejectsUnsafeTemplates(t *testing.T) { + values := []string{ + "file:///srv/repository/{version}", + "https://user:password@example.invalid/{version}", + "https://example.invalid/{releasever}/{architecture}", + "https://example.invalid/{version", + } + for _, value := range values { + spec := validSpec() + spec.URLTemplate = value + if _, err := NormalizeSpec(spec); err == nil { + t.Fatalf("NormalizeSpec(%q) error = nil", value) + } + } +} diff --git a/manager/backend/services/footstone/infrastructure/config/config.go b/manager/backend/services/footstone/infrastructure/config/config.go index 0b9ad1b0ddafebe3a1e29840e4959892d3dbc058..1169d70a55160f560ce15a021560bbdd347a4777 100644 --- a/manager/backend/services/footstone/infrastructure/config/config.go +++ b/manager/backend/services/footstone/infrastructure/config/config.go @@ -97,7 +97,7 @@ type ApplicationConfig struct { MetaDB config.AccessPoint `json:"metadb" yaml:"metadb"` ClickhouseDB ClickhouseConfig `yaml:"clickhouse_db"` // 系统基础信息指标队列 - SysInfoMq ConsumerMq `yaml:"sys_info_mq"` + SysInfoMq ConsumerMq `yaml:"sys_info_mq"` InstanceCache config.AccessPoint `yaml:"redis_cache"` Lock config.AccessPoint `json:"lock" yaml:"redis_lock"` } @@ -111,13 +111,212 @@ type ApplicationConfig struct { } Stream struct { MaxConnection int - BatchStoreSize int `json:"batch_store_size" yaml:"batch_store_size"` - SysInfo config.CronPoint `yaml:"sys_info"` - MonitorSweeper MonitorSweeperConfig `yaml:"monitor_sweeper"` + BatchStoreSize int `json:"batch_store_size" yaml:"batch_store_size"` + SysInfo config.CronPoint `yaml:"sys_info"` + MonitorSweeper MonitorSweeperConfig `yaml:"monitor_sweeper"` } + Repository RepositoryConfig `yaml:"repository"` } } +// RepositoryConfig controls private repository staging. Published content is +// configured in Phase 4; private keys must never be placed below Root. +type RepositoryConfig struct { + Root string `yaml:"root"` + PublicBaseURL string `yaml:"public_base_url"` + TMSBaseURL string `yaml:"tms_base_url"` + TMSUserName string `yaml:"tms_user_name"` + MaxUploadBytes int64 `yaml:"max_upload_bytes"` + MaxChunkBytes int `yaml:"max_chunk_bytes"` + MaxArchiveFiles int `yaml:"max_archive_files"` + MaxExpandedBytes int64 `yaml:"max_expanded_bytes"` + MaxSyncBytes int64 `yaml:"max_sync_bytes"` + MaxSyncPackages int `yaml:"max_sync_packages"` + FilteredSyncBytes int64 `yaml:"filtered_sync_bytes"` + FilteredSyncPackages int `yaml:"filtered_sync_packages"` + MetadataReserve int64 `yaml:"metadata_reserve_bytes"` + UploadTTLHours int `yaml:"upload_ttl_hours"` + MinimumFreeBytes int64 `yaml:"minimum_free_bytes"` + MetadataSigningKeyRoot string `yaml:"metadata_signing_key_root"` + InternalControl RepositoryInternalControlConfig `yaml:"internal_control"` + NodeBootstrap RepositoryNodeBootstrapConfig `yaml:"node_bootstrap"` + NodeArtifactRoot string `yaml:"node_artifact_root"` + NodeClient RepositoryNodeClientConfig `yaml:"node_client"` + NodeServer RepositoryNodeServerConfig `yaml:"node_server"` + NodeDataClient RepositoryNodeDataClientConfig `yaml:"node_data_client"` +} + +// RepositoryInternalControlConfig exposes node-only RPCs on a dedicated mTLS listener. +// Certificate private keys are deployment files and must never be stored in MySQL or repository-data. +type RepositoryInternalControlConfig struct { + Enabled bool `yaml:"enabled"` + ListenAddress string `yaml:"listen_address"` + PublicTarget string `yaml:"public_target"` + ServerCert string `yaml:"server_cert"` + ServerKey string `yaml:"server_key"` + ServerCA string `yaml:"server_ca"` + ServerName string `yaml:"server_name"` + ClientCA string `yaml:"client_ca"` +} + +// RepositoryNodeBootstrapConfig exposes a token-authenticated TLS endpoint for first enrollment. +// The signing key is a read-only deployment secret and is never stored in MySQL or repository-data. +type RepositoryNodeBootstrapConfig struct { + Enabled bool `yaml:"enabled"` + ListenAddress string `yaml:"listen_address"` + PublicTarget string `yaml:"public_target"` + ServerCert string `yaml:"server_cert"` + ServerKey string `yaml:"server_key"` + ServerCA string `yaml:"server_ca"` + ServerName string `yaml:"server_name"` + SigningCACert string `yaml:"signing_ca_cert"` + SigningCAKey string `yaml:"signing_ca_key"` + CertificateDays int `yaml:"certificate_days"` + RenewBeforeDays int `yaml:"renew_before_days"` + CertificateGraceHours int `yaml:"certificate_grace_hours"` + EnrollmentTTLMinutes int `yaml:"enrollment_ttl_minutes"` +} + +// RepositoryNodeClientConfig connects a DB-free repository worker to Footstone. +// Client credentials are mounted deployment files outside repository-data. +type RepositoryNodeClientConfig struct { + StorageNodeID string `yaml:"storage_node_id"` + ControlTarget string `yaml:"control_target"` + ClientCert string `yaml:"client_cert"` + ClientKey string `yaml:"client_key"` + ServerCA string `yaml:"server_ca"` + ServerName string `yaml:"server_name"` + HeartbeatSeconds int `yaml:"heartbeat_seconds"` + RPCSeconds int `yaml:"rpc_timeout_seconds"` + BootstrapURL string `yaml:"bootstrap_url"` + BootstrapServerCA string `yaml:"bootstrap_server_ca"` + BootstrapServerName string `yaml:"bootstrap_server_name"` + BootstrapTokenFile string `yaml:"bootstrap_token_file"` + EnrollmentID string `yaml:"enrollment_id"` + InstallTaskID string `yaml:"install_task_id"` + TMSAgentID uint64 `yaml:"tms_agent_id"` +} + +// RepositoryNodeServerConfig exposes repository-node data operations to Footstone over mTLS. +type RepositoryNodeServerConfig struct { + Enabled bool `yaml:"enabled"` + ListenAddress string `yaml:"listen_address"` + ServerCert string `yaml:"server_cert"` + ServerKey string `yaml:"server_key"` + ClientCA string `yaml:"client_ca"` + MaxResultBytes int64 `yaml:"max_result_bytes"` + ReadTimeoutSeconds int `yaml:"read_timeout_seconds"` +} + +// RepositoryNodeDataClientConfig is the Footstone-side client for repository-node data operations. +type RepositoryNodeDataClientConfig struct { + Enabled bool `yaml:"enabled"` + ClientCert string `yaml:"client_cert"` + ClientKey string `yaml:"client_key"` + ServerCA string `yaml:"server_ca"` + TimeoutSeconds int `yaml:"timeout_seconds"` + MaxResultBytes int64 `yaml:"max_result_bytes"` +} + +func (c RepositoryConfig) WithDefaults() RepositoryConfig { + if c.Root == "" { + c.Root = "/var/lib/ocmanager/repository-data" + } + if c.NodeArtifactRoot == "" { + c.NodeArtifactRoot = "/var/lib/ocmanager/repository-node-artifacts" + } + if c.MetadataSigningKeyRoot == "" { + c.MetadataSigningKeyRoot = "/etc/ocmanager/repository-metadata-signing" + } + if c.MaxUploadBytes <= 0 { + c.MaxUploadBytes = 2 << 30 + } + if c.TMSUserName == "" { + c.TMSUserName = "ocmanager-repository" + } + if c.NodeBootstrap.CertificateDays <= 0 { + c.NodeBootstrap.CertificateDays = 90 + } + if c.NodeBootstrap.RenewBeforeDays <= 0 { + c.NodeBootstrap.RenewBeforeDays = 30 + } + if c.NodeBootstrap.CertificateGraceHours <= 0 { + c.NodeBootstrap.CertificateGraceHours = 24 + } + if c.NodeBootstrap.EnrollmentTTLMinutes <= 0 { + c.NodeBootstrap.EnrollmentTTLMinutes = 15 + } + if c.MaxChunkBytes <= 0 { + c.MaxChunkBytes = 4 << 20 + } + if c.MaxArchiveFiles <= 0 { + c.MaxArchiveFiles = 1000 + } + if c.MaxExpandedBytes <= 0 { + c.MaxExpandedBytes = 8 << 30 + } + if c.MaxSyncBytes <= 0 { + c.MaxSyncBytes = 64 << 30 + } + if c.MaxSyncPackages <= 0 { + c.MaxSyncPackages = 100000 + } + if c.FilteredSyncBytes <= 0 { + c.FilteredSyncBytes = 2 << 30 + } + if c.FilteredSyncPackages <= 0 { + c.FilteredSyncPackages = 1000 + } + if c.MetadataReserve <= 0 { + c.MetadataReserve = 512 << 20 + } + if c.UploadTTLHours <= 0 { + c.UploadTTLHours = 24 + } + if c.MinimumFreeBytes <= 0 { + c.MinimumFreeBytes = 1 << 30 + } + if c.NodeClient.HeartbeatSeconds <= 0 { + c.NodeClient.HeartbeatSeconds = 10 + } + if c.NodeClient.RPCSeconds <= 0 { + c.NodeClient.RPCSeconds = 10 + } + if c.NodeServer.ListenAddress == "" { + c.NodeServer.ListenAddress = "0.0.0.0:19443" + } + if c.NodeServer.MaxResultBytes <= 0 { + c.NodeServer.MaxResultBytes = 64 << 20 + } + if c.NodeServer.ReadTimeoutSeconds <= 0 { + c.NodeServer.ReadTimeoutSeconds = 30 + } + if c.NodeDataClient.TimeoutSeconds <= 0 { + c.NodeDataClient.TimeoutSeconds = 30 + } + if c.NodeDataClient.MaxResultBytes <= 0 { + c.NodeDataClient.MaxResultBytes = 64 << 20 + } + return c +} + +// SyncLimits returns the configured task ceiling. The full-sync values remain +// the hard administrator ceiling for every synchronization mode. +func (c RepositoryConfig) SyncLimits(filtered bool) (int64, int) { + c = c.WithDefaults() + if !filtered { + return c.MaxSyncBytes, c.MaxSyncPackages + } + bytes, packages := c.FilteredSyncBytes, c.FilteredSyncPackages + if bytes > c.MaxSyncBytes { + bytes = c.MaxSyncBytes + } + if packages > c.MaxSyncPackages { + packages = c.MaxSyncPackages + } + return bytes, packages +} + // MonitorSweeperConfig os_monitor 离线扫描配置。 type MonitorSweeperConfig struct { Enable bool `yaml:"enable"` diff --git a/manager/backend/services/footstone/infrastructure/config/load_test.go b/manager/backend/services/footstone/infrastructure/config/load_test.go index 7751f29fa669ba7e1e435e1767437ee4578d9088..5d6686219a8b206af88e1a33850fd8a678924b04 100644 --- a/manager/backend/services/footstone/infrastructure/config/load_test.go +++ b/manager/backend/services/footstone/infrastructure/config/load_test.go @@ -29,3 +29,24 @@ func TestLoadConfigFromRainbow(t *testing.T) { fmt.Println(appentity) } + +func TestRepositoryConfigWithDefaultsUsesTieredSyncLimits(t *testing.T) { + config := (RepositoryConfig{}).WithDefaults() + fullBytes, fullPackages := config.SyncLimits(false) + if fullBytes != 64<<30 || fullPackages != 100000 { + t.Fatalf("full sync limits = %d/%d", fullBytes, fullPackages) + } + filteredBytes, filteredPackages := config.SyncLimits(true) + if filteredBytes != 2<<30 || filteredPackages != 1000 { + t.Fatalf("filtered sync limits = %d/%d", filteredBytes, filteredPackages) + } +} + +func TestRepositoryConfigSyncLimitsHonorAdministratorCeiling(t *testing.T) { + config := RepositoryConfig{MaxSyncBytes: 512, MaxSyncPackages: 20, + FilteredSyncBytes: 1024, FilteredSyncPackages: 40} + bytes, packages := config.SyncLimits(true) + if bytes != 512 || packages != 20 { + t.Fatalf("filtered sync limits = %d/%d, want administrator ceiling 512/20", bytes, packages) + } +} diff --git a/manager/backend/services/footstone/infrastructure/repository/nodeclient/client.go b/manager/backend/services/footstone/infrastructure/repository/nodeclient/client.go new file mode 100644 index 0000000000000000000000000000000000000000..7a51b8d532d70157020524377dd9b7aa50082118 --- /dev/null +++ b/manager/backend/services/footstone/infrastructure/repository/nodeclient/client.go @@ -0,0 +1,586 @@ +package nodeclient + +import ( + "bytes" + "context" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" + + "github.com/google/uuid" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/nodeprotocol" +) + +var digestPattern = regexp.MustCompile(`^[0-9a-f]{64}$`) + +type Client struct { + tlsConfig *tls.Config + timeout time.Duration + maxBytes int64 +} + +func (c *Client) Probe(ctx context.Context, node *po.RepositoryStorageNode) ( + *nodeprotocol.ProbeResponse, error, +) { + if c == nil || c.tlsConfig == nil || node == nil { + return nil, errors.New("repository node probe client is not configured") + } + httpClient, transport, endpoint, err := c.requestClient(node, nodeprotocol.ProbePath) + if err != nil { + return nil, err + } + defer transport.CloseIdleConnections() + httpRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + response, err := httpClient.Do(httpRequest) + if err != nil { + return nil, fmt.Errorf("probe repository node: %w", err) + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK { + return nil, nodeHTTPError(response) + } + value := new(nodeprotocol.ProbeResponse) + decoder := json.NewDecoder(io.LimitReader(response.Body, 128<<10)) + decoder.DisallowUnknownFields() + if err = decoder.Decode(value); err != nil || decoder.Decode(new(interface{})) != io.EOF { + return nil, errors.New("repository node returned an invalid probe response") + } + if value.CapacityBytes <= 0 || value.FreeBytes < 0 || value.FreeBytes > value.CapacityBytes || + value.FreeInodes < 0 || strings.TrimSpace(value.FilesystemType) == "" || value.ObservedAt.IsZero() || + len(value.ToolVersions) > 64 { + return nil, errors.New("repository node returned inconsistent probe metadata") + } + return value, nil +} + +func New(cfg config.RepositoryNodeDataClientConfig, repositoryRoot string) (*Client, error) { + if !cfg.Enabled { + return nil, errors.New("repository node data client is disabled") + } + if cfg.ClientCert == "" || cfg.ClientKey == "" || cfg.ServerCA == "" { + return nil, errors.New("repository node data client mTLS configuration is incomplete") + } + for name, value := range map[string]string{"client certificate": cfg.ClientCert, + "client key": cfg.ClientKey, "server CA": cfg.ServerCA} { + absolute, err := filepath.Abs(value) + if err != nil || absolute != filepath.Clean(value) { + return nil, fmt.Errorf("repository node %s path must be absolute", name) + } + if insidePath(absolute, repositoryRoot) { + return nil, fmt.Errorf("repository node %s must be outside repository-data", name) + } + } + certificate, err := tls.LoadX509KeyPair(cfg.ClientCert, cfg.ClientKey) + if err != nil { + return nil, fmt.Errorf("load repository node client certificate: %w", err) + } + caBytes, err := os.ReadFile(cfg.ServerCA) + if err != nil { + return nil, fmt.Errorf("read repository node server CA: %w", err) + } + roots := x509.NewCertPool() + if !roots.AppendCertsFromPEM(caBytes) { + return nil, errors.New("repository node server CA does not contain a certificate") + } + if cfg.TimeoutSeconds <= 0 { + cfg.TimeoutSeconds = 30 + } + if cfg.MaxResultBytes <= 0 { + cfg.MaxResultBytes = 64 << 20 + } + return &Client{tlsConfig: &tls.Config{Certificates: []tls.Certificate{certificate}, RootCAs: roots, + MinVersion: tls.VersionTLS12}, timeout: time.Duration(cfg.TimeoutSeconds) * time.Second, + maxBytes: cfg.MaxResultBytes}, nil +} + +func (c *Client) ReadTaskResult(ctx context.Context, node *po.RepositoryStorageNode, taskID, objectID, + expectedSHA256 string, +) ([]byte, error) { + if c == nil || c.tlsConfig == nil || node == nil { + return nil, errors.New("repository node result client is not configured") + } + if uuid.Validate(taskID) != nil || !digestPattern.MatchString(expectedSHA256) { + return nil, errors.New("repository node result request identity is invalid") + } + wantedObjectID, _ := nodeprotocol.TaskResultObjectID(taskID) + if objectID != wantedObjectID { + return nil, errors.New("repository node result object identity is invalid") + } + httpClient, transport, endpoint, err := c.requestClient(node, nodeprotocol.TaskResultPathPrefix+taskID) + if err != nil { + return nil, err + } + defer transport.CloseIdleConnections() + httpRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + httpRequest.Header.Set(nodeprotocol.HeaderObjectID, objectID) + response, err := httpClient.Do(httpRequest) + if err != nil { + return nil, fmt.Errorf("read repository node result: %w", err) + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK { + _, _ = io.Copy(io.Discard, io.LimitReader(response.Body, 4096)) + return nil, fmt.Errorf("repository node result returned HTTP %d", response.StatusCode) + } + if response.Header.Get(nodeprotocol.HeaderObjectID) != objectID || + response.Header.Get(nodeprotocol.HeaderSHA256) != expectedSHA256 { + return nil, errors.New("repository node result response identity or digest mismatch") + } + if response.ContentLength < 0 || response.ContentLength > c.maxBytes { + return nil, errors.New("repository node result response size is invalid") + } + limited := io.LimitReader(response.Body, c.maxBytes+1) + content, err := io.ReadAll(limited) + if err != nil { + return nil, fmt.Errorf("read repository node result body: %w", err) + } + if int64(len(content)) > c.maxBytes || int64(len(content)) != response.ContentLength { + return nil, errors.New("repository node result response exceeded its declared bounds") + } + digest := sha256.Sum256(content) + if hex.EncodeToString(digest[:]) != expectedSHA256 { + return nil, errors.New("repository node result body digest mismatch") + } + return content, nil +} + +func (c *Client) ReadTaskLogMetadata(ctx context.Context, node *po.RepositoryStorageNode, + taskID string, +) (*nodeprotocol.TaskLogMetadata, error) { + if c == nil || c.tlsConfig == nil || node == nil || uuid.Validate(taskID) != nil { + return nil, errors.New("repository node task log request identity is invalid") + } + httpClient, transport, endpoint, err := c.requestClient(node, + nodeprotocol.TaskLogPathPrefix+taskID+"/metadata") + if err != nil { + return nil, err + } + defer transport.CloseIdleConnections() + httpRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + response, err := httpClient.Do(httpRequest) + if err != nil { + return nil, fmt.Errorf("read repository node task log metadata: %w", err) + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK { + return nil, nodeHTTPError(response) + } + value := new(nodeprotocol.TaskLogMetadata) + decoder := json.NewDecoder(io.LimitReader(response.Body, 128<<10)) + decoder.DisallowUnknownFields() + if err = decoder.Decode(value); err != nil || decoder.Decode(new(interface{})) != io.EOF { + return nil, errors.New("repository node returned invalid task log metadata") + } + wantedPath, _ := nodeprotocol.TaskLogRelativePath(taskID) + if value.SchemaVersion != 1 || value.TaskID != taskID || value.RelativePath != wantedPath || + !digestPattern.MatchString(value.SHA256) || value.CompressedBytes <= 0 || + value.UncompressedBytes <= 0 || value.LineCount <= 0 || len(value.Summary) > 64<<10 || + (value.ErrorFingerprint != "" && !digestPattern.MatchString(value.ErrorFingerprint)) || + value.ExpiresAt.IsZero() || value.CreatedAt.IsZero() { + return nil, errors.New("repository node returned inconsistent task log metadata") + } + return value, nil +} + +func (c *Client) OpenTaskLog(ctx context.Context, node *po.RepositoryStorageNode, taskID, + relativePath, expectedSHA256 string, expectedSize int64, +) (io.ReadCloser, error) { + if c == nil || c.tlsConfig == nil || node == nil || uuid.Validate(taskID) != nil || + !digestPattern.MatchString(expectedSHA256) || expectedSize <= 0 { + return nil, errors.New("repository node task log download identity is invalid") + } + wantedPath, _ := nodeprotocol.TaskLogRelativePath(taskID) + if relativePath != wantedPath { + return nil, errors.New("repository node task log download path is invalid") + } + httpClient, transport, endpoint, err := c.requestClient(node, + nodeprotocol.TaskLogPathPrefix+taskID+"/content") + if err != nil { + return nil, err + } + // The caller's context controls a potentially large streaming download. + httpClient.Timeout = 0 + httpRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + transport.CloseIdleConnections() + return nil, err + } + response, err := httpClient.Do(httpRequest) + if err != nil { + transport.CloseIdleConnections() + return nil, fmt.Errorf("open repository node task log: %w", err) + } + if response.StatusCode != http.StatusOK || response.Header.Get(nodeprotocol.HeaderObjectID) != relativePath || + response.Header.Get(nodeprotocol.HeaderSHA256) != expectedSHA256 || response.ContentLength != expectedSize { + response.Body.Close() + transport.CloseIdleConnections() + return nil, errors.New("repository node task log response identity is invalid") + } + return &transportReadCloser{ReadCloser: response.Body, transport: transport}, nil +} + +type transportReadCloser struct { + io.ReadCloser + transport *http.Transport +} + +func (r *transportReadCloser) Close() error { + err := r.ReadCloser.Close() + r.transport.CloseIdleConnections() + return err +} + +func (c *Client) BeginUpload(ctx context.Context, node *po.RepositoryStorageNode, uploadID string, + expectedSize int64, +) (string, error) { + if uuid.Validate(uploadID) != nil || expectedSize <= 0 { + return "", errors.New("repository node upload identity or size is invalid") + } + requestBody, _ := json.Marshal(nodeprotocol.BeginUploadRequest{ExpectedSize: expectedSize}) + var result nodeprotocol.BeginUploadResponse + if err := c.doJSON(ctx, node, http.MethodPost, nodeprotocol.UploadPathPrefix+uploadID+"/begin", + requestBody, http.StatusCreated, &result); err != nil { + return "", err + } + wanted, _ := nodeprotocol.UploadRelativePath(uploadID) + if result.RelativePath != wanted { + return "", errors.New("repository node returned an inconsistent upload path") + } + return result.RelativePath, nil +} + +func (c *Client) AppendUploadChunk(ctx context.Context, node *po.RepositoryStorageNode, uploadID string, + expectedSize, offset int64, data []byte, +) (int64, error) { + if uuid.Validate(uploadID) != nil || expectedSize <= 0 || offset < 0 || len(data) == 0 || int64(len(data)) > 4<<20 { + return 0, errors.New("repository node upload chunk metadata is invalid") + } + httpClient, transport, endpoint, err := c.requestClient(node, nodeprotocol.UploadPathPrefix+uploadID+ + "/content?expected_size="+strconv.FormatInt(expectedSize, 10)) + if err != nil { + return 0, err + } + defer transport.CloseIdleConnections() + httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPut, endpoint, bytes.NewReader(data)) + if err != nil { + return 0, err + } + digest := sha256.Sum256(data) + httpRequest.Header.Set(nodeprotocol.HeaderChunkOffset, strconv.FormatInt(offset, 10)) + httpRequest.Header.Set(nodeprotocol.HeaderChunkLength, strconv.Itoa(len(data))) + httpRequest.Header.Set(nodeprotocol.HeaderChunkSHA256, hex.EncodeToString(digest[:])) + response, err := httpClient.Do(httpRequest) + if err != nil { + return 0, fmt.Errorf("append repository node upload chunk: %w", err) + } + defer response.Body.Close() + if response.StatusCode != http.StatusNoContent { + return 0, nodeHTTPError(response) + } + next, err := strconv.ParseInt(response.Header.Get(nodeprotocol.HeaderNextOffset), 10, 64) + if err != nil || next != offset+int64(len(data)) { + return 0, errors.New("repository node returned an inconsistent upload offset") + } + return next, nil +} + +func (c *Client) PutUploadChunk(ctx context.Context, node *po.RepositoryStorageNode, uploadID string, + chunkIndex int, data []byte, expectedSHA256 string, +) (string, error) { + if uuid.Validate(uploadID) != nil || chunkIndex < 0 || len(data) == 0 || int64(len(data)) > 4<<20 || + !digestPattern.MatchString(expectedSHA256) { + return "", errors.New("repository node upload chunk metadata is invalid") + } + endpointPath := nodeprotocol.UploadPathPrefix + uploadID + "/chunks/" + strconv.Itoa(chunkIndex) + httpClient, transport, endpoint, err := c.requestClient(node, endpointPath) + if err != nil { + return "", err + } + defer transport.CloseIdleConnections() + httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPut, endpoint, bytes.NewReader(data)) + if err != nil { + return "", err + } + httpRequest.Header.Set(nodeprotocol.HeaderChunkLength, strconv.Itoa(len(data))) + httpRequest.Header.Set(nodeprotocol.HeaderChunkSHA256, expectedSHA256) + response, err := httpClient.Do(httpRequest) + if err != nil { + return "", fmt.Errorf("put repository node upload chunk: %w", err) + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK { + return "", nodeHTTPError(response) + } + var result nodeprotocol.PutUploadChunkResponse + if err = json.NewDecoder(io.LimitReader(response.Body, 4097)).Decode(&result); err != nil { + return "", fmt.Errorf("decode repository node upload chunk result: %w", err) + } + wanted, _ := nodeprotocol.UploadChunkRelativePath(uploadID, chunkIndex) + if result.RelativePath != wanted { + return "", errors.New("repository node returned an inconsistent upload chunk path") + } + return result.RelativePath, nil +} + +func (c *Client) CompleteUpload(ctx context.Context, node *po.RepositoryStorageNode, uploadID string, + expectedSize int64, expectedSHA256 string, totalChunks ...int, +) (*nodeprotocol.CompleteUploadResponse, error) { + if uuid.Validate(uploadID) != nil || expectedSize <= 0 || + (expectedSHA256 != "" && !digestPattern.MatchString(expectedSHA256)) { + return nil, errors.New("repository node upload completion expectation is invalid") + } + chunkCount := 0 + if len(totalChunks) > 0 { + chunkCount = totalChunks[0] + } + requestBody, _ := json.Marshal(nodeprotocol.CompleteUploadRequest{ + ExpectedSize: expectedSize, ExpectedSHA256: expectedSHA256, TotalChunks: chunkCount}) + result := new(nodeprotocol.CompleteUploadResponse) + if err := c.doJSON(ctx, node, http.MethodPost, nodeprotocol.UploadPathPrefix+uploadID+"/complete", + requestBody, http.StatusOK, result); err != nil { + return nil, err + } + wanted, _ := nodeprotocol.UploadRelativePath(uploadID) + if result.RelativePath != wanted || result.SizeBytes != expectedSize || !digestPattern.MatchString(result.SHA256) || + (expectedSHA256 != "" && result.SHA256 != expectedSHA256) { + return nil, errors.New("repository node returned inconsistent upload completion metadata") + } + return result, nil +} + +func (c *Client) AbortUpload(ctx context.Context, node *po.RepositoryStorageNode, uploadID string) error { + if uuid.Validate(uploadID) != nil { + return errors.New("repository node upload identity is invalid") + } + httpClient, transport, endpoint, err := c.requestClient(node, nodeprotocol.UploadPathPrefix+uploadID) + if err != nil { + return err + } + defer transport.CloseIdleConnections() + httpRequest, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint, nil) + if err != nil { + return err + } + response, err := httpClient.Do(httpRequest) + if err != nil { + return fmt.Errorf("abort repository node upload: %w", err) + } + defer response.Body.Close() + if response.StatusCode != http.StatusNoContent { + return nodeHTTPError(response) + } + return nil +} + +func (c *Client) PutGPGPublicKey(ctx context.Context, node *po.RepositoryStorageNode, + repositoryID, publishKey, fingerprint, keyID string, content []byte, +) (*nodeprotocol.GPGKeyResponse, error) { + endpointPath, err := nodeprotocol.GPGKeyPath(repositoryID, fingerprint) + if err != nil || len(content) == 0 || len(content) > 1<<20 || publishKey == "" || keyID == "" { + return nil, errors.New("repository GPG key request metadata is invalid") + } + digest := sha256.Sum256(content) + expectedSHA := hex.EncodeToString(digest[:]) + httpClient, transport, endpoint, err := c.requestClient(node, endpointPath) + if err != nil { + return nil, err + } + defer transport.CloseIdleConnections() + httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPut, endpoint, bytes.NewReader(content)) + if err != nil { + return nil, err + } + httpRequest.Header.Set("Content-Type", "application/pgp-keys") + httpRequest.Header.Set(nodeprotocol.HeaderPublishKey, publishKey) + httpRequest.Header.Set(nodeprotocol.HeaderGPGKeyID, strings.ToUpper(keyID)) + httpRequest.Header.Set(nodeprotocol.HeaderGPGKeySHA256, expectedSHA) + response, err := httpClient.Do(httpRequest) + if err != nil { + return nil, fmt.Errorf("store repository GPG public key: %w", err) + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK { + return nil, nodeHTTPError(response) + } + result := new(nodeprotocol.GPGKeyResponse) + decoder := json.NewDecoder(io.LimitReader(response.Body, 1<<20)) + decoder.DisallowUnknownFields() + if err = decoder.Decode(result); err != nil || decoder.Decode(new(interface{})) != io.EOF { + return nil, errors.New("repository node returned an invalid GPG key response") + } + wantedPrivate := filepath.ToSlash(filepath.Join("keys", repositoryID, fingerprint, "public.key")) + wantedPublic := filepath.ToSlash(filepath.Join("public", publishKey, "keys", fingerprint, "public.key")) + if result.RepositoryID != repositoryID || result.Fingerprint != fingerprint || + result.KeyID != strings.ToUpper(keyID) || result.PrivatePath != wantedPrivate || + result.PublicPath != wantedPublic || result.ContentSHA256 != expectedSHA { + return nil, errors.New("repository node returned inconsistent GPG key metadata") + } + return result, nil +} + +func (c *Client) VerifyGPGPublicKey(ctx context.Context, node *po.RepositoryStorageNode, + repositoryID, publishKey, fingerprint, expectedPrivatePath string, +) (string, error) { + endpointPath, err := nodeprotocol.GPGKeyPath(repositoryID, fingerprint) + if err != nil || publishKey == "" || expectedPrivatePath == "" { + return "", errors.New("repository GPG key verification metadata is invalid") + } + httpClient, transport, endpoint, err := c.requestClient(node, endpointPath) + if err != nil { + return "", err + } + defer transport.CloseIdleConnections() + httpRequest, err := http.NewRequestWithContext(ctx, http.MethodHead, endpoint, nil) + if err != nil { + return "", err + } + httpRequest.Header.Set(nodeprotocol.HeaderPublishKey, publishKey) + response, err := httpClient.Do(httpRequest) + if err != nil { + return "", fmt.Errorf("verify repository GPG public key: %w", err) + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK { + return "", nodeHTTPError(response) + } + wantedPublic := filepath.ToSlash(filepath.Join("public", publishKey, "keys", fingerprint, "public.key")) + if response.Header.Get(nodeprotocol.HeaderPrivatePath) != expectedPrivatePath || + response.Header.Get(nodeprotocol.HeaderPublicPath) != wantedPublic || + !digestPattern.MatchString(response.Header.Get(nodeprotocol.HeaderGPGKeySHA256)) { + return "", errors.New("repository node returned inconsistent GPG key verification metadata") + } + return publicGPGKeyURL(node.PublishBaseURL, publishKey, fingerprint) +} + +func (c *Client) RemoveGPGPublicKey(ctx context.Context, node *po.RepositoryStorageNode, + repositoryID, publishKey, fingerprint string, +) error { + endpointPath, err := nodeprotocol.GPGKeyPath(repositoryID, fingerprint) + if err != nil || publishKey == "" { + return errors.New("repository GPG key removal metadata is invalid") + } + httpClient, transport, endpoint, err := c.requestClient(node, endpointPath) + if err != nil { + return err + } + defer transport.CloseIdleConnections() + httpRequest, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint, nil) + if err != nil { + return err + } + httpRequest.Header.Set(nodeprotocol.HeaderPublishKey, publishKey) + response, err := httpClient.Do(httpRequest) + if err != nil { + return fmt.Errorf("remove repository GPG public key: %w", err) + } + defer response.Body.Close() + if response.StatusCode != http.StatusNoContent { + return nodeHTTPError(response) + } + return nil +} + +func publicGPGKeyURL(baseURL, publishKey, fingerprint string) (string, error) { + value, err := url.Parse(strings.TrimSpace(baseURL)) + if err != nil || (value.Scheme != "http" && value.Scheme != "https") || value.Host == "" || + value.User != nil || value.RawQuery != "" || value.Fragment != "" { + return "", errors.New("repository node publish URL is invalid") + } + value.Path = strings.TrimRight(value.Path, "/") + "/" + publishKey + "/keys/" + fingerprint + "/public.key" + value.RawPath = "" + return value.String(), nil +} + +func (c *Client) doJSON(ctx context.Context, node *po.RepositoryStorageNode, method, suffix string, + requestBody []byte, wantedStatus int, destination interface{}, +) error { + httpClient, transport, endpoint, err := c.requestClient(node, suffix) + if err != nil { + return err + } + defer transport.CloseIdleConnections() + httpRequest, err := http.NewRequestWithContext(ctx, method, endpoint, bytes.NewReader(requestBody)) + if err != nil { + return err + } + httpRequest.Header.Set("Content-Type", "application/json") + response, err := httpClient.Do(httpRequest) + if err != nil { + return fmt.Errorf("call repository node data API: %w", err) + } + defer response.Body.Close() + if response.StatusCode != wantedStatus { + return nodeHTTPError(response) + } + decoder := json.NewDecoder(io.LimitReader(response.Body, 1<<20)) + decoder.DisallowUnknownFields() + if err = decoder.Decode(destination); err != nil { + return fmt.Errorf("decode repository node response: %w", err) + } + return nil +} + +func (c *Client) requestClient(node *po.RepositoryStorageNode, suffix string) ( + *http.Client, *http.Transport, string, error, +) { + if c == nil || c.tlsConfig == nil || node == nil { + return nil, nil, "", errors.New("repository node data client is not configured") + } + base, err := url.Parse(node.ControlBaseURL) + if err != nil || base.Scheme != "https" || base.Host == "" || base.User != nil || + base.RawQuery != "" || base.Fragment != "" { + return nil, nil, "", errors.New("repository node control URL is invalid") + } + parsedSuffix, err := url.Parse(suffix) + if err != nil || !strings.HasPrefix(parsedSuffix.Path, "/") { + return nil, nil, "", errors.New("repository node API path is invalid") + } + base.Path = strings.TrimRight(base.Path, "/") + parsedSuffix.Path + base.RawQuery = parsedSuffix.RawQuery + tlsConfig := c.tlsConfig.Clone() + tlsConfig.ServerName = node.TLSServerName + transport := &http.Transport{TLSClientConfig: tlsConfig, Proxy: http.ProxyFromEnvironment} + return &http.Client{Transport: transport, Timeout: c.timeout}, transport, base.String(), nil +} + +func nodeHTTPError(response *http.Response) error { + _, _ = io.Copy(io.Discard, io.LimitReader(response.Body, 4096)) + return fmt.Errorf("repository node data API returned HTTP %d", response.StatusCode) +} + +func insidePath(candidate, root string) bool { + if strings.TrimSpace(root) == "" { + return false + } + rootAbsolute, err := filepath.Abs(root) + if err != nil { + return false + } + relative, err := filepath.Rel(rootAbsolute, candidate) + return err == nil && relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator)) +} diff --git a/manager/backend/services/footstone/infrastructure/repository/nodeclient/client_test.go b/manager/backend/services/footstone/infrastructure/repository/nodeclient/client_test.go new file mode 100644 index 0000000000000000000000000000000000000000..1044914f39f89d1f74987f3765bbbd87eb5d2f6d --- /dev/null +++ b/manager/backend/services/footstone/infrastructure/repository/nodeclient/client_test.go @@ -0,0 +1,325 @@ +package nodeclient + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/hex" + "encoding/json" + "encoding/pem" + "math/big" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/nodeprotocol" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/repositoryworker/nodeapi" +) + +const nodeClientTestTaskID = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" + +func TestProbeOverMTLS(t *testing.T) { + root := t.TempDir() + caCert, caKey, caPath := createCA(t, root) + serverCert, serverKey := issueCertificate(t, root, "probe-server", caCert, caKey, false) + clientCert, clientKey := issueCertificate(t, root, "probe-client", caCert, caKey, true) + handler := http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { + if request.URL.Path != nodeprotocol.ProbePath || request.TLS == nil || len(request.TLS.VerifiedChains) == 0 { + http.Error(response, "mTLS required", http.StatusUnauthorized) + return + } + _ = json.NewEncoder(response).Encode(nodeprotocol.ProbeResponse{FilesystemType: "xfs", + CapacityBytes: 1000, FreeBytes: 700, FreeInodes: 20000, + ToolVersions: map[string]string{"rpm": "4.16"}, ObservedAt: time.Now().UTC()}) + }) + server := httptest.NewUnstartedServer(handler) + serverCertificate, err := tls.LoadX509KeyPair(serverCert, serverKey) + if err != nil { + t.Fatal(err) + } + clientCAs := x509.NewCertPool() + clientCAs.AddCert(caCert) + server.TLS = &tls.Config{Certificates: []tls.Certificate{serverCertificate}, ClientCAs: clientCAs, + ClientAuth: tls.RequireAndVerifyClientCert, MinVersion: tls.VersionTLS12} + server.StartTLS() + defer server.Close() + client, err := New(config.RepositoryNodeDataClientConfig{Enabled: true, ClientCert: clientCert, + ClientKey: clientKey, ServerCA: caPath, TimeoutSeconds: 5}, filepath.Join(root, "repository-data")) + if err != nil { + t.Fatal(err) + } + value, err := client.Probe(context.Background(), &po.RepositoryStorageNode{ + ControlBaseURL: server.URL, TLSServerName: "repository-node.test", + }) + if err != nil || value.FilesystemType != "xfs" || value.FreeBytes != 700 { + t.Fatalf("Probe() value/error = %#v/%v", value, err) + } +} + +func TestReadTaskResultOverMTLS(t *testing.T) { + root := t.TempDir() + caCert, caKey, caPath := createCA(t, root) + serverCert, serverKey := issueCertificate(t, root, "server", caCert, caKey, false) + clientCert, clientKey := issueCertificate(t, root, "client", caCert, caKey, true) + body := []byte("repository result fixture") + digest := sha256.Sum256(body) + expectedSHA := hex.EncodeToString(digest[:]) + objectID, _ := nodeprotocol.TaskResultObjectID(nodeClientTestTaskID) + handler := http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { + if request.TLS == nil || len(request.TLS.VerifiedChains) == 0 { + http.Error(response, "mTLS required", http.StatusUnauthorized) + return + } + response.Header().Set(nodeprotocol.HeaderObjectID, request.Header.Get(nodeprotocol.HeaderObjectID)) + response.Header().Set(nodeprotocol.HeaderSHA256, expectedSHA) + response.WriteHeader(http.StatusOK) + _, _ = response.Write(body) + }) + server := httptest.NewUnstartedServer(handler) + serverCertificate, err := tls.LoadX509KeyPair(serverCert, serverKey) + if err != nil { + t.Fatal(err) + } + clientCAs := x509.NewCertPool() + clientCAs.AddCert(caCert) + server.TLS = &tls.Config{Certificates: []tls.Certificate{serverCertificate}, ClientCAs: clientCAs, + ClientAuth: tls.RequireAndVerifyClientCert, MinVersion: tls.VersionTLS12} + server.StartTLS() + defer server.Close() + client, err := New(config.RepositoryNodeDataClientConfig{Enabled: true, ClientCert: clientCert, + ClientKey: clientKey, ServerCA: caPath, TimeoutSeconds: 5, MaxResultBytes: 1024}, + filepath.Join(root, "repository-data")) + if err != nil { + t.Fatal(err) + } + node := &po.RepositoryStorageNode{ControlBaseURL: server.URL, TLSServerName: "repository-node.test"} + content, err := client.ReadTaskResult(context.Background(), node, nodeClientTestTaskID, objectID, expectedSHA) + if err != nil { + t.Fatal(err) + } + if string(content) != string(body) { + t.Fatalf("result body = %q", content) + } + _, err = client.ReadTaskResult(context.Background(), node, nodeClientTestTaskID, objectID, strings.Repeat("f", 64)) + if err == nil || !strings.Contains(err.Error(), "digest mismatch") { + t.Fatalf("digest mismatch error = %v", err) + } +} + +func TestUploadLifecycleOverMTLS(t *testing.T) { + root := t.TempDir() + caCert, caKey, caPath := createCA(t, root) + serverCert, serverKey := issueCertificate(t, root, "upload-server", caCert, caKey, false) + clientCert, clientKey := issueCertificate(t, root, "upload-client", caCert, caKey, true) + store, err := storage.New(config.RepositoryConfig{Root: filepath.Join(root, "repository-data"), + MinimumFreeBytes: 1, MaxUploadBytes: 1024, MaxChunkBytes: 16}) + if err != nil { + t.Fatal(err) + } + nodeServer, err := nodeapi.New(config.RepositoryNodeServerConfig{}, store) + if err != nil { + t.Fatal(err) + } + server := httptest.NewUnstartedServer(nodeServer.Handler()) + serverCertificate, err := tls.LoadX509KeyPair(serverCert, serverKey) + if err != nil { + t.Fatal(err) + } + clientCAs := x509.NewCertPool() + clientCAs.AddCert(caCert) + server.TLS = &tls.Config{Certificates: []tls.Certificate{serverCertificate}, ClientCAs: clientCAs, + ClientAuth: tls.RequireAndVerifyClientCert, MinVersion: tls.VersionTLS12} + server.StartTLS() + defer server.Close() + client, err := New(config.RepositoryNodeDataClientConfig{Enabled: true, ClientCert: clientCert, + ClientKey: clientKey, ServerCA: caPath, TimeoutSeconds: 5, MaxResultBytes: 1024}, store.Root()) + if err != nil { + t.Fatal(err) + } + node := &po.RepositoryStorageNode{ControlBaseURL: server.URL, TLSServerName: "repository-node.test"} + content := []byte("upload fixture") + digest := sha256.Sum256(content) + digestHex := hex.EncodeToString(digest[:]) + relative, err := client.BeginUpload(context.Background(), node, nodeClientTestTaskID, int64(len(content))) + if err != nil { + t.Fatal(err) + } + wantedRelative, _ := nodeprotocol.UploadRelativePath(nodeClientTestTaskID) + if relative != wantedRelative { + t.Fatalf("upload relative path = %q", relative) + } + next, err := client.AppendUploadChunk(context.Background(), node, nodeClientTestTaskID, + int64(len(content)), 0, content) + if err != nil || next != int64(len(content)) { + t.Fatalf("AppendUploadChunk() next/error = %d/%v", next, err) + } + completed, err := client.CompleteUpload(context.Background(), node, nodeClientTestTaskID, + int64(len(content)), digestHex) + if err != nil || completed.SHA256 != digestHex { + t.Fatalf("CompleteUpload() result/error = %#v/%v", completed, err) + } + if err = client.AbortUpload(context.Background(), node, nodeClientTestTaskID); err != nil { + t.Fatal(err) + } + if _, err = os.Stat(filepath.Join(store.Root(), "trash", "aborted-uploads", nodeClientTestTaskID, "content")); err != nil { + t.Fatalf("aborted upload was not moved to trash: %v", err) + } + indexedID := "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" + if _, err = client.BeginUpload(context.Background(), node, indexedID, int64(len(content))); err != nil { + t.Fatal(err) + } + chunkPath, err := client.PutUploadChunk(context.Background(), node, indexedID, 0, content, digestHex) + wantedChunkPath, _ := nodeprotocol.UploadChunkRelativePath(indexedID, 0) + if err != nil || chunkPath != wantedChunkPath { + t.Fatalf("PutUploadChunk() path/error = %q/%v", chunkPath, err) + } + indexedCompleted, err := client.CompleteUpload(context.Background(), node, indexedID, + int64(len(content)), digestHex, 1) + if err != nil || indexedCompleted.SHA256 != digestHex { + t.Fatalf("indexed CompleteUpload() result/error = %#v/%v", indexedCompleted, err) + } +} + +func TestGPGKeyLifecycleOverMTLS(t *testing.T) { + root := t.TempDir() + caCert, caKey, caPath := createCA(t, root) + serverCert, serverKey := issueCertificate(t, root, "gpg-server", caCert, caKey, false) + clientCert, clientKey := issueCertificate(t, root, "gpg-client", caCert, caKey, true) + store, err := storage.New(config.RepositoryConfig{Root: filepath.Join(root, "repository-data"), + MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + nodeServer, err := nodeapi.New(config.RepositoryNodeServerConfig{}, store) + if err != nil { + t.Fatal(err) + } + server := httptest.NewUnstartedServer(nodeServer.Handler()) + serverCertificate, err := tls.LoadX509KeyPair(serverCert, serverKey) + if err != nil { + t.Fatal(err) + } + clientCAs := x509.NewCertPool() + clientCAs.AddCert(caCert) + server.TLS = &tls.Config{Certificates: []tls.Certificate{serverCertificate}, ClientCAs: clientCAs, + ClientAuth: tls.RequireAndVerifyClientCert, MinVersion: tls.VersionTLS12} + server.StartTLS() + defer server.Close() + client, err := New(config.RepositoryNodeDataClientConfig{Enabled: true, ClientCert: clientCert, + ClientKey: clientKey, ServerCA: caPath, TimeoutSeconds: 5, MaxResultBytes: 1024}, store.Root()) + if err != nil { + t.Fatal(err) + } + node := &po.RepositoryStorageNode{ControlBaseURL: server.URL, TLSServerName: "repository-node.test", + PublishBaseURL: "http://repository-node.test:18090/repositories"} + repositoryID := "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" + fingerprint := "0123456789ABCDEF0123456789ABCDEF01234567" + keyID := "89ABCDEF01234567" + publicKey := []byte("-----BEGIN PGP PUBLIC KEY BLOCK-----\nfixture\n-----END PGP PUBLIC KEY BLOCK-----\n") + stored, err := client.PutGPGPublicKey(context.Background(), node, repositoryID, "private-repo", + fingerprint, keyID, publicKey) + if err != nil { + t.Fatal(err) + } + publicURL, err := client.VerifyGPGPublicKey(context.Background(), node, repositoryID, "private-repo", + fingerprint, stored.PrivatePath) + if err != nil || publicURL != "http://repository-node.test:18090/repositories/private-repo/keys/"+ + fingerprint+"/public.key" { + t.Fatalf("VerifyGPGPublicKey() URL/error = %q/%v", publicURL, err) + } + if err = client.RemoveGPGPublicKey(context.Background(), node, repositoryID, "private-repo", fingerprint); err != nil { + t.Fatal(err) + } + if _, err = client.VerifyGPGPublicKey(context.Background(), node, repositoryID, "private-repo", + fingerprint, stored.PrivatePath); err == nil { + t.Fatal("VerifyGPGPublicKey() found a removed key") + } +} + +func TestNewRejectsPrivateKeyInsideRepositoryData(t *testing.T) { + root := t.TempDir() + _, _, caPath := createCA(t, root) + _, err := New(config.RepositoryNodeDataClientConfig{Enabled: true, + ClientCert: caPath, ClientKey: filepath.Join(root, "repository-data", "client.key"), ServerCA: caPath}, + filepath.Join(root, "repository-data")) + if err == nil || !strings.Contains(err.Error(), "outside repository-data") { + t.Fatalf("New() private key error = %v", err) + } +} + +func createCA(t *testing.T, root string) (*x509.Certificate, *rsa.PrivateKey, string) { + t.Helper() + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + template := &x509.Certificate{SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "repository-test-ca"}, + NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour), IsCA: true, + BasicConstraintsValid: true, KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature} + der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + if err != nil { + t.Fatal(err) + } + certificate, err := x509.ParseCertificate(der) + if err != nil { + t.Fatal(err) + } + path := filepath.Join(root, "ca.crt") + writePEM(t, path, "CERTIFICATE", der, 0600) + return certificate, key, path +} + +func issueCertificate(t *testing.T, root, name string, ca *x509.Certificate, caKey *rsa.PrivateKey, + client bool, +) (string, string) { + t.Helper() + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + usage := []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth} + dnsNames := []string{"repository-node.test"} + if client { + usage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth} + dnsNames = nil + } + template := &x509.Certificate{SerialNumber: big.NewInt(time.Now().UnixNano()), + Subject: pkix.Name{CommonName: name}, NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: usage, DNSNames: dnsNames} + der, err := x509.CreateCertificate(rand.Reader, template, ca, &key.PublicKey, caKey) + if err != nil { + t.Fatal(err) + } + certificatePath := filepath.Join(root, name+".crt") + keyPath := filepath.Join(root, name+".key") + writePEM(t, certificatePath, "CERTIFICATE", der, 0600) + writePEM(t, keyPath, "RSA PRIVATE KEY", x509.MarshalPKCS1PrivateKey(key), 0600) + return certificatePath, keyPath +} + +func writePEM(t *testing.T, path, blockType string, bytes []byte, mode os.FileMode) { + t.Helper() + file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, mode) + if err != nil { + t.Fatal(err) + } + if err = pem.Encode(file, &pem.Block{Type: blockType, Bytes: bytes}); err != nil { + _ = file.Close() + t.Fatal(err) + } + if err = file.Close(); err != nil { + t.Fatal(err) + } +} diff --git a/manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer.go b/manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer.go new file mode 100644 index 0000000000000000000000000000000000000000..82b466fd77147cd28aa1ef21cab0763f2345aad6 --- /dev/null +++ b/manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer.go @@ -0,0 +1,256 @@ +package nodeinstaller + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "crypto/sha256" + "encoding/base64" + "errors" + "fmt" + "net/url" + "path/filepath" + "strconv" + "strings" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +var ( + ErrTargetNotFound = errors.New("repository node TMS target was not found") + ErrTargetOffline = errors.New("repository node TMS target is offline") +) + +type Client interface { + ListNodes(context.Context) ([]request.RepositoryDistributionNode, error) + UploadArtifact(context.Context, string, []byte) (string, error) + PublishWithArgs(context.Context, string, string, []string, int) (string, error) +} + +// RepositorySource pins installation to an immutable Repository revision. The +// exact package URL is downloaded only for SHA-256 verification; DNF installs +// the requested NEVRA through BaseURL so evidence identifies the Repository as +// the package source. +type RepositorySource struct { + BaseURL string + PackageURL string + GPGCheck bool + GPGKeyURL string + RepoGPGCheck bool +} + +type BootstrapPayload struct { + Token string + NodeID string + EnrollmentID string + InstallTaskID string + TMSAgentID uint64 + BootstrapTarget string + ControlTarget string + BootstrapServerName string + BootstrapServerCA []byte + ControlServerName string + ControlServerCA []byte + DataClientCA []byte +} + +type Dispatcher struct{ client Client } + +func New(client Client) *Dispatcher { return &Dispatcher{client: client} } + +func (d *Dispatcher) Dispatch(ctx context.Context, artifact *po.RepositoryNodeArtifact, + source RepositorySource, payload BootstrapPayload, timeoutSeconds int, +) (string, error) { + if d == nil || d.client == nil || artifact == nil { + return "", errors.New("repository node installer is not configured") + } + if err := validatePayload(payload); err != nil { + return "", err + } + if err := validateRepositoryArtifact(artifact, source); err != nil { + return "", err + } + nodes, err := d.client.ListNodes(ctx) + if err != nil { + return "", err + } + targetIP := "" + for _, node := range nodes { + if node.AgentID != payload.TMSAgentID { + continue + } + if !node.Connected { + return "", ErrTargetOffline + } + targetIP = strings.TrimSpace(node.IP) + break + } + if targetIP == "" { + return "", ErrTargetNotFound + } + bundle, err := buildBundle() + if err != nil { + return "", err + } + // TMS carries only this small installer script. The RPM remains in the + // immutable standard YUM/DNF Repository and never enters the TMS artifact. + artifactMD5, err := d.client.UploadArtifact(ctx, "repository-node-install-script.tar.gz", bundle) + if err != nil { + return "", fmt.Errorf("upload repository node installer to TMS: %w", err) + } + argsB64 := base64.StdEncoding.EncodeToString(encodePayload(payload, artifact, source)) + if timeoutSeconds <= 0 { + timeoutSeconds = 900 + } + taskID, err := d.client.PublishWithArgs(ctx, artifactMD5, argsB64, []string{targetIP}, timeoutSeconds) + if err != nil { + return "", fmt.Errorf("publish repository node installer through TMS: %w", err) + } + return taskID, nil +} + +func buildBundle() ([]byte, error) { + var output bytes.Buffer + gzipWriter := gzip.NewWriter(&output) + tarWriter := tar.NewWriter(gzipWriter) + runScript := `#!/bin/sh +set -eu +payload_b64=${1:-} +if [ -z "$payload_b64" ]; then + echo "missing repository node bootstrap payload" >&2 + exit 64 +fi +work_dir=$(mktemp -d) +payload_file=${work_dir}/bootstrap.payload +package_file=${work_dir}/node.rpm +cleanup() { rm -rf -- "$work_dir"; } +trap cleanup EXIT HUP INT TERM +printf '%s' "$payload_b64" | base64 -d >"$payload_file" +chmod 0600 "$payload_file" + +decode_line() { sed -n "$1p" "$payload_file" | base64 -d; } +repo_url=$(decode_line 14) +package_url=$(decode_line 15) +nevra=$(decode_line 16) +package_name=$(decode_line 17) +package_sha256=$(decode_line 18) +gpgcheck=$(decode_line 19) +gpgkey=$(decode_line 20) +repo_gpgcheck=$(decode_line 21) +expected_arch=$(decode_line 22) + +case "$repo_url" in http://*|https://*) ;; *) echo "invalid immutable repository URL" >&2; exit 65;; esac +case "$package_url" in http://*|https://*) ;; *) echo "invalid immutable package URL" >&2; exit 65;; esac +case "$package_sha256" in *[!0-9a-f]*|'') echo "invalid package SHA-256" >&2; exit 65;; esac +[ "${#package_sha256}" -eq 64 ] || { echo "invalid package SHA-256" >&2; exit 65; } +case "$gpgcheck:$repo_gpgcheck" in 0:0|0:1|1:0|1:1) ;; *) echo "invalid GPG policy" >&2; exit 65;; esac +actual_arch=$(uname -m) +case "$actual_arch" in amd64|x64|x86-64) actual_arch=x86_64;; arm64|arm64-v8a) actual_arch=aarch64;; esac +[ "$actual_arch" = "$expected_arch" ] || { echo "target architecture does not match Node RPM" >&2; exit 65; } +if [ "$gpgcheck" = 1 ] || [ "$repo_gpgcheck" = 1 ]; then + [ -n "$gpgkey" ] || { echo "missing repository GPG key URL" >&2; exit 65; } +fi +if [ -n "$gpgkey" ]; then + for key_url in $gpgkey; do + case "$key_url" in http://*|https://*) ;; *) echo "invalid repository GPG key URL" >&2; exit 65;; esac + done +fi + +curl --fail --location --silent --show-error "$package_url" --output "$package_file" +printf '%s %s\n' "$package_sha256" "$package_file" | sha256sum -c - + +set -- -y --setopt=install_weak_deps=False \ + --repofrompath="ocmanager-node-rpm,$repo_url" \ + --setopt="ocmanager-node-rpm.gpgcheck=$gpgcheck" \ + --setopt="ocmanager-node-rpm.repo_gpgcheck=$repo_gpgcheck" +if [ -n "$gpgkey" ]; then + set -- "$@" --setopt="ocmanager-node-rpm.gpgkey=$gpgkey" +fi +LC_ALL=C LANG=C dnf "$@" install "$nevra" +rpm -q "$package_name" >/dev/null +/usr/libexec/ocmanager/repository-node-configure "$payload_file" +systemctl enable ocmanager-repository-node.service ocmanager-repository-publisher.service +systemctl restart ocmanager-repository-node.service ocmanager-repository-publisher.service +` + content := []byte(runScript) + if err := tarWriter.WriteHeader(&tar.Header{Name: "run.sh", Mode: 0755, + Size: int64(len(content)), Typeflag: tar.TypeReg}); err != nil { + return nil, err + } + if _, err := tarWriter.Write(content); err != nil { + return nil, err + } + if err := tarWriter.Close(); err != nil { + return nil, err + } + if err := gzipWriter.Close(); err != nil { + return nil, err + } + return output.Bytes(), nil +} + +func encodePayload(value BootstrapPayload, artifact *po.RepositoryNodeArtifact, source RepositorySource) []byte { + fields := [][]byte{[]byte(value.Token), []byte(value.NodeID), []byte(value.EnrollmentID), + []byte(value.InstallTaskID), []byte(strconv.FormatUint(value.TMSAgentID, 10)), + []byte(value.BootstrapTarget), []byte(value.BootstrapServerName), value.BootstrapServerCA, + []byte(value.ControlTarget), []byte(value.ControlServerName), value.ControlServerCA, value.DataClientCA, + []byte(source.BaseURL), []byte(source.PackageURL), []byte(artifact.NEVRA), []byte(artifact.Name), + []byte(strings.ToLower(artifact.SHA256)), []byte(boolDigit(source.GPGCheck)), []byte(source.GPGKeyURL), + []byte(boolDigit(source.RepoGPGCheck)), []byte(artifact.Architecture)} + var output strings.Builder + output.WriteString("ocmanager-repository-node-bootstrap/v1\n") + for _, field := range fields { + output.WriteString(base64.StdEncoding.EncodeToString(field)) + output.WriteByte('\n') + } + return []byte(output.String()) +} + +func boolDigit(value bool) string { + if value { + return "1" + } + return "0" +} + +func validateRepositoryArtifact(artifact *po.RepositoryNodeArtifact, source RepositorySource) error { + if artifact.SourceKind != request.RepositoryNodeArtifactRepository || + artifact.Status != request.RepositoryNodeArtifactPublished || + artifact.Name != "ocmanager-repository-node" || strings.TrimSpace(artifact.NEVRA) == "" || + artifact.SizeBytes <= 0 || len(artifact.SHA256) != sha256.Size*2 || + filepath.Base(artifact.FileName) != artifact.FileName { + return errors.New("repository-backed Node RPM artifact metadata is invalid") + } + for _, raw := range []string{source.BaseURL, source.PackageURL} { + parsed, err := url.Parse(strings.TrimSpace(raw)) + if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" || parsed.User != nil { + return errors.New("immutable Node RPM repository URL is invalid") + } + } + if source.GPGCheck || source.RepoGPGCheck { + if strings.TrimSpace(source.GPGKeyURL) == "" { + return errors.New("Node RPM repository GPG key URL is invalid") + } + } + for _, raw := range strings.Fields(source.GPGKeyURL) { + parsed, err := url.Parse(raw) + if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" || parsed.User != nil { + return errors.New("Node RPM repository GPG key URL is invalid") + } + } + return nil +} + +func validatePayload(value BootstrapPayload) error { + if len(value.Token) < 32 || strings.TrimSpace(value.NodeID) == "" || + strings.TrimSpace(value.EnrollmentID) == "" || strings.TrimSpace(value.InstallTaskID) == "" || + value.TMSAgentID == 0 || strings.TrimSpace(value.BootstrapTarget) == "" || + strings.TrimSpace(value.BootstrapServerName) == "" || len(value.BootstrapServerCA) == 0 || + strings.TrimSpace(value.ControlTarget) == "" || strings.TrimSpace(value.ControlServerName) == "" || + len(value.ControlServerCA) == 0 || len(value.DataClientCA) == 0 { + return errors.New("repository node bootstrap payload is incomplete") + } + return nil +} diff --git a/manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer_test.go b/manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer_test.go new file mode 100644 index 0000000000000000000000000000000000000000..6c7b991bbb5dcbe27df3a5607394dd9720a5890a --- /dev/null +++ b/manager/backend/services/footstone/infrastructure/repository/nodeinstaller/installer_test.go @@ -0,0 +1,154 @@ +package nodeinstaller + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "encoding/base64" + "errors" + "io" + "strings" + "testing" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +func TestDispatchUsesImmutableRepositoryAndKeepsRPMOutOfTMSArtifact(t *testing.T) { + client := &fakeInstallerClient{nodes: []request.RepositoryDistributionNode{{ + AgentID: 99, IP: "192.0.2.25", Connected: true, + }}} + artifact, source, payload := validRepositoryArtifact(), validRepositorySource(), validBootstrapPayload() + taskID, err := New(client).Dispatch(context.Background(), artifact, source, payload, 300) + if err != nil || taskID != "tms-task" { + t.Fatalf("Dispatch() = %q, %v", taskID, err) + } + if len(client.artifact) > 64<<10 { + t.Fatalf("TMS installer unexpectedly large: %d bytes", len(client.artifact)) + } + runScript := readBundleScript(t, client.artifact) + for _, required := range []string{"--repofrompath=", "sha256sum -c", "gpgcheck", "repo_gpgcheck", + "trap cleanup EXIT", "dnf \"$@\" install \"$nevra\"", + "systemctl enable ocmanager-repository-node.service ocmanager-repository-publisher.service", + "systemctl restart ocmanager-repository-node.service ocmanager-repository-publisher.service"} { + if !strings.Contains(runScript, required) { + t.Fatalf("installer script missing %q:\n%s", required, runScript) + } + } + if strings.Contains(runScript, "--nogpgcheck") || strings.Contains(runScript, "dnf install ./") { + t.Fatalf("installer bypasses Repository policy or uses bundled RPM:\n%s", runScript) + } + decoded, err := base64.StdEncoding.DecodeString(client.args) + if err != nil { + t.Fatal(err) + } + text := string(decoded) + for _, value := range []string{source.BaseURL, source.PackageURL, artifact.NEVRA, artifact.SHA256} { + if !payloadContains(text, value) { + t.Fatalf("encoded payload does not contain %q", value) + } + } + if strings.Contains(string(client.artifact), payload.Token) { + t.Fatal("bootstrap token leaked into reusable TMS artifact") + } +} + +func TestDispatchRejectsLegacyArtifactAndMissingGPGKey(t *testing.T) { + client := &fakeInstallerClient{nodes: []request.RepositoryDistributionNode{{ + AgentID: 99, IP: "192.0.2.25", Connected: true, + }}} + artifact := validRepositoryArtifact() + artifact.SourceKind = request.RepositoryNodeArtifactLegacyLocal + if _, err := New(client).Dispatch(context.Background(), artifact, validRepositorySource(), + validBootstrapPayload(), 0); err == nil { + t.Fatal("Dispatch() accepted LEGACY_LOCAL artifact") + } + artifact = validRepositoryArtifact() + source := validRepositorySource() + source.GPGKeyURL = "" + if _, err := New(client).Dispatch(context.Background(), artifact, source, + validBootstrapPayload(), 0); err == nil { + t.Fatal("Dispatch() accepted gpgcheck without a key URL") + } +} + +func TestDispatchRejectsOfflineTarget(t *testing.T) { + client := &fakeInstallerClient{nodes: []request.RepositoryDistributionNode{{AgentID: 99, Connected: false}}} + _, err := New(client).Dispatch(context.Background(), validRepositoryArtifact(), validRepositorySource(), + validBootstrapPayload(), 0) + if !errors.Is(err, ErrTargetOffline) { + t.Fatalf("Dispatch() error = %v, want ErrTargetOffline", err) + } +} + +func validRepositoryArtifact() *po.RepositoryNodeArtifact { + return &po.RepositoryNodeArtifact{ID: "artifact", ProjectID: 42, + SourceKind: request.RepositoryNodeArtifactRepository, Status: request.RepositoryNodeArtifactPublished, + Name: "ocmanager-repository-node", Version: "2.0.0", Release: "1", Architecture: "x86_64", + NEVRA: "ocmanager-repository-node-0:2.0.0-1.x86_64", + FileName: "ocmanager-repository-node-2.0.0-1.x86_64.rpm", + SizeBytes: 1024, SHA256: strings.Repeat("a", 64)} +} + +func validRepositorySource() RepositorySource { + return RepositorySource{BaseURL: "https://repo.example/repositories/key/revisions/rev", + PackageURL: "https://repo.example/repositories/key/revisions/rev/Packages/ocmanager-repository-node.rpm", + GPGCheck: true, GPGKeyURL: "https://repo.example/repositories/key/keys/public.key", RepoGPGCheck: true} +} + +func validBootstrapPayload() BootstrapPayload { + return BootstrapPayload{Token: strings.Repeat("t", 40), NodeID: "node-id", + EnrollmentID: "enrollment-id", InstallTaskID: "install-task-id", TMSAgentID: 99, + BootstrapTarget: "ip://manager:19444", BootstrapServerName: "manager.example", + BootstrapServerCA: []byte("bootstrap-ca"), ControlTarget: "ip://manager:19443", + ControlServerName: "manager.example", ControlServerCA: []byte("control-ca"), + DataClientCA: []byte("data-ca")} +} + +func readBundleScript(t *testing.T, bundle []byte) string { + t.Helper() + gzipReader, err := gzip.NewReader(bytes.NewReader(bundle)) + if err != nil { + t.Fatal(err) + } + defer gzipReader.Close() + tarReader := tar.NewReader(gzipReader) + header, err := tarReader.Next() + if err != nil || header.Name != "run.sh" { + t.Fatalf("bundle entry = %#v, %v", header, err) + } + content, err := io.ReadAll(tarReader) + if err != nil { + t.Fatal(err) + } + if _, err = tarReader.Next(); err != io.EOF { + t.Fatalf("bundle contains an unexpected second entry: %v", err) + } + return string(content) +} + +func payloadContains(payload, value string) bool { + encoded := base64.StdEncoding.EncodeToString([]byte(value)) + return strings.Contains(payload, "\n"+encoded+"\n") +} + +type fakeInstallerClient struct { + nodes []request.RepositoryDistributionNode + artifact []byte + args string +} + +func (f *fakeInstallerClient) ListNodes(context.Context) ([]request.RepositoryDistributionNode, error) { + return f.nodes, nil +} +func (f *fakeInstallerClient) UploadArtifact(_ context.Context, _ string, content []byte) (string, error) { + f.artifact = append([]byte(nil), content...) + return "artifact-md5", nil +} +func (f *fakeInstallerClient) PublishWithArgs(_ context.Context, _ string, args string, _ []string, + _ int, +) (string, error) { + f.args = args + return "tms-task", nil +} diff --git a/manager/backend/services/footstone/infrastructure/repository/nodepki/issuer.go b/manager/backend/services/footstone/infrastructure/repository/nodepki/issuer.go new file mode 100644 index 0000000000000000000000000000000000000000..07440894ce763cb27e7548a4ba4209ca4d70c70b --- /dev/null +++ b/manager/backend/services/footstone/infrastructure/repository/nodepki/issuer.go @@ -0,0 +1,146 @@ +package nodepki + +import ( + "crypto" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "crypto/x509/pkix" + "encoding/hex" + "encoding/pem" + "errors" + "fmt" + "math/big" + "net" + "net/url" + "os" + "strings" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" +) + +type Issuer struct { + ca *x509.Certificate + privateKey crypto.Signer + caPEM []byte + lifetime time.Duration + now func() time.Time +} + +func New(certificatePath, privateKeyPath string, lifetime time.Duration) (*Issuer, error) { + if lifetime <= 0 { + lifetime = 90 * 24 * time.Hour + } + certificatePEM, err := os.ReadFile(strings.TrimSpace(certificatePath)) + if err != nil { + return nil, fmt.Errorf("read repository node signing CA certificate: %w", err) + } + privateKeyPEM, err := os.ReadFile(strings.TrimSpace(privateKeyPath)) + if err != nil { + return nil, fmt.Errorf("read repository node signing CA private key: %w", err) + } + certificateBlock, _ := pem.Decode(certificatePEM) + if certificateBlock == nil || certificateBlock.Type != "CERTIFICATE" { + return nil, errors.New("repository node signing CA certificate is invalid") + } + ca, err := x509.ParseCertificate(certificateBlock.Bytes) + if err != nil || !ca.IsCA || ca.KeyUsage&x509.KeyUsageCertSign == 0 { + return nil, errors.New("repository node signing CA is not permitted to sign certificates") + } + keyBlock, _ := pem.Decode(privateKeyPEM) + if keyBlock == nil { + return nil, errors.New("repository node signing CA private key is invalid") + } + privateKey, err := parsePrivateKey(keyBlock.Bytes) + if err != nil { + return nil, err + } + if !publicKeysEqual(ca.PublicKey, privateKey.Public()) { + return nil, errors.New("repository node signing CA certificate and private key do not match") + } + return &Issuer{ca: ca, privateKey: privateKey, caPEM: append([]byte(nil), certificatePEM...), + lifetime: lifetime, now: func() time.Time { return time.Now().UTC() }}, nil +} + +func (i *Issuer) CACertificatePEM() []byte { return append([]byte(nil), i.caPEM...) } + +func (i *Issuer) Issue(csrPEM []byte, nodeID, serverName string) (request.RepositoryIssuedCertificate, error) { + if i == nil || i.ca == nil || i.privateKey == nil { + return request.RepositoryIssuedCertificate{}, errors.New("repository node certificate issuer is unavailable") + } + block, _ := pem.Decode(csrPEM) + if block == nil || block.Type != "CERTIFICATE REQUEST" { + return request.RepositoryIssuedCertificate{}, errors.New("repository node CSR is invalid") + } + csr, err := x509.ParseCertificateRequest(block.Bytes) + if err != nil || csr.CheckSignature() != nil { + return request.RepositoryIssuedCertificate{}, errors.New("repository node CSR signature is invalid") + } + if err = validatePublicKey(csr.PublicKey); err != nil { + return request.RepositoryIssuedCertificate{}, err + } + now := i.now().UTC() + serialLimit := new(big.Int).Lsh(big.NewInt(1), 160) + serial, err := rand.Int(rand.Reader, serialLimit) + if err != nil { + return request.RepositoryIssuedCertificate{}, fmt.Errorf("generate repository node certificate serial: %w", err) + } + identity, _ := url.Parse("urn:ocmanager:repository-node:" + nodeID) + serverName = strings.TrimSpace(serverName) + if serverName == "" { + return request.RepositoryIssuedCertificate{}, errors.New("repository node TLS server name is required") + } + template := &x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{CommonName: "repository-node:" + nodeID, Organization: []string{"OCManager"}}, + NotBefore: now.Add(-5 * time.Minute), NotAfter: now.Add(i.lifetime), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + URIs: []*url.URL{identity}, BasicConstraintsValid: true, + } + if address := net.ParseIP(serverName); address != nil { + template.IPAddresses = []net.IP{address} + } else { + template.DNSNames = []string{serverName} + } + der, err := x509.CreateCertificate(rand.Reader, template, i.ca, csr.PublicKey, i.privateKey) + if err != nil { + return request.RepositoryIssuedCertificate{}, fmt.Errorf("issue repository node certificate: %w", err) + } + digest := sha256.Sum256(der) + return request.RepositoryIssuedCertificate{ + PEM: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), + SerialNumber: serial.Text(16), Fingerprint: hex.EncodeToString(digest[:]), + NotBefore: template.NotBefore, NotAfter: template.NotAfter, + }, nil +} + +func parsePrivateKey(der []byte) (crypto.Signer, error) { + if key, err := x509.ParsePKCS8PrivateKey(der); err == nil { + if signer, ok := key.(crypto.Signer); ok { + return signer, nil + } + } + if key, err := x509.ParsePKCS1PrivateKey(der); err == nil { + return key, nil + } + if key, err := x509.ParseECPrivateKey(der); err == nil { + return key, nil + } + return nil, errors.New("repository node signing CA private key format is unsupported") +} + +func publicKeysEqual(left, right any) bool { + leftDER, leftErr := x509.MarshalPKIXPublicKey(left) + rightDER, rightErr := x509.MarshalPKIXPublicKey(right) + return leftErr == nil && rightErr == nil && string(leftDER) == string(rightDER) +} + +func validatePublicKey(value any) error { + der, err := x509.MarshalPKIXPublicKey(value) + if err != nil || len(der) < 64 { + return errors.New("repository node CSR public key is unsupported") + } + return nil +} diff --git a/manager/backend/services/footstone/infrastructure/repository/nodepki/issuer_test.go b/manager/backend/services/footstone/infrastructure/repository/nodepki/issuer_test.go new file mode 100644 index 0000000000000000000000000000000000000000..4eb1cb3ff82f07a779728a829f7e3ab6ac64d3c1 --- /dev/null +++ b/manager/backend/services/footstone/infrastructure/repository/nodepki/issuer_test.go @@ -0,0 +1,142 @@ +package nodepki + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "crypto/x509/pkix" + "encoding/hex" + "encoding/pem" + "math/big" + "os" + "path/filepath" + "testing" + "time" +) + +const testRepositoryNodeID = "33333333-3333-4333-8333-333333333333" + +func TestIssuerIssuesNodeBoundClientCertificate(t *testing.T) { + certificatePath, keyPath, ca := writeTestCA(t) + issuer, err := New(certificatePath, keyPath, 90*24*time.Hour) + if err != nil { + t.Fatal(err) + } + now := time.Date(2026, 9, 20, 12, 0, 0, 0, time.UTC) + issuer.now = func() time.Time { return now } + csr := testCSR(t) + + issued, err := issuer.Issue(csr, testRepositoryNodeID, "repo-node.example") + if err != nil { + t.Fatal(err) + } + block, _ := pem.Decode(issued.PEM) + if block == nil { + t.Fatal("issued certificate PEM is invalid") + } + certificate, err := x509.ParseCertificate(block.Bytes) + if err != nil { + t.Fatal(err) + } + roots := x509.NewCertPool() + roots.AddCert(ca) + if _, err = certificate.Verify(x509.VerifyOptions{ + Roots: roots, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + CurrentTime: now, + }); err != nil { + t.Fatalf("verify issued certificate: %v", err) + } + wantIdentity := "urn:ocmanager:repository-node:" + testRepositoryNodeID + if len(certificate.URIs) != 1 || certificate.URIs[0].String() != wantIdentity { + t.Fatalf("certificate identity = %v, want %q", certificate.URIs, wantIdentity) + } + if len(certificate.DNSNames) != 1 || certificate.DNSNames[0] != "repo-node.example" { + t.Fatalf("certificate DNS names = %v", certificate.DNSNames) + } + digest := sha256.Sum256(block.Bytes) + if issued.Fingerprint != hex.EncodeToString(digest[:]) { + t.Fatalf("fingerprint = %q", issued.Fingerprint) + } + if !issued.NotAfter.Equal(now.Add(90 * 24 * time.Hour)) { + t.Fatalf("not_after = %s", issued.NotAfter) + } +} + +func TestIssuerRejectsInvalidCSR(t *testing.T) { + certificatePath, keyPath, _ := writeTestCA(t) + issuer, err := New(certificatePath, keyPath, time.Hour) + if err != nil { + t.Fatal(err) + } + if _, err = issuer.Issue([]byte("not a csr"), testRepositoryNodeID, "repo-node.example"); err == nil { + t.Fatal("Issue() accepted an invalid CSR") + } +} + +func TestNewRejectsMismatchedCAKey(t *testing.T) { + certificatePath, _, _ := writeTestCA(t) + otherKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + der, err := x509.MarshalPKCS8PrivateKey(otherKey) + if err != nil { + t.Fatal(err) + } + keyPath := filepath.Join(t.TempDir(), "wrong-ca.key") + if err = os.WriteFile(keyPath, pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der}), 0600); err != nil { + t.Fatal(err) + } + if _, err = New(certificatePath, keyPath, time.Hour); err == nil { + t.Fatal("New() accepted a mismatched CA private key") + } +} + +func writeTestCA(t *testing.T) (string, string, *x509.Certificate) { + t.Helper() + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + now := time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC) + template := &x509.Certificate{SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "test-ca"}, + NotBefore: now.Add(-time.Hour), NotAfter: now.AddDate(2, 0, 0), IsCA: true, + BasicConstraintsValid: true, KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature} + der, err := x509.CreateCertificate(rand.Reader, template, template, &privateKey.PublicKey, privateKey) + if err != nil { + t.Fatal(err) + } + certificate, err := x509.ParseCertificate(der) + if err != nil { + t.Fatal(err) + } + keyDER, err := x509.MarshalPKCS8PrivateKey(privateKey) + if err != nil { + t.Fatal(err) + } + directory := t.TempDir() + certificatePath, keyPath := filepath.Join(directory, "ca.crt"), filepath.Join(directory, "ca.key") + if err = os.WriteFile(certificatePath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), 0644); err != nil { + t.Fatal(err) + } + if err = os.WriteFile(keyPath, pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}), 0600); err != nil { + t.Fatal(err) + } + return certificatePath, keyPath, certificate +} + +func testCSR(t *testing.T) []byte { + t.Helper() + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + der, err := x509.CreateCertificateRequest(rand.Reader, + &x509.CertificateRequest{Subject: pkix.Name{CommonName: "repository-node"}}, privateKey) + if err != nil { + t.Fatal(err) + } + return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: der}) +} diff --git a/manager/backend/services/footstone/infrastructure/repository/nodeprotocol/protocol.go b/manager/backend/services/footstone/infrastructure/repository/nodeprotocol/protocol.go new file mode 100644 index 0000000000000000000000000000000000000000..92bd25621d2652f0803bd16b57bf78a19aa5d4f9 --- /dev/null +++ b/manager/backend/services/footstone/infrastructure/repository/nodeprotocol/protocol.go @@ -0,0 +1,154 @@ +package nodeprotocol + +import ( + "fmt" + "path" + "time" + + "github.com/google/uuid" +) + +const ( + ProbePath = "/internal/v1/probe" + TaskResultPathPrefix = "/internal/v1/task-results/" + HeaderObjectID = "X-Repository-Object-ID" + HeaderSHA256 = "X-Repository-Result-SHA256" + UploadPathPrefix = "/internal/v1/uploads/" + HeaderChunkOffset = "X-Repository-Chunk-Offset" + HeaderChunkLength = "X-Repository-Chunk-Length" + HeaderChunkSHA256 = "X-Repository-Chunk-SHA256" + HeaderNextOffset = "X-Repository-Next-Offset" + HeaderChunkIndex = "X-Repository-Chunk-Index" + GPGKeyPathPrefix = "/internal/v1/gpg-keys/" + TaskLogPathPrefix = "/internal/v1/task-logs/" + HeaderPublishKey = "X-Repository-Publish-Key" + HeaderGPGKeyID = "X-Repository-GPG-Key-ID" + HeaderGPGKeySHA256 = "X-Repository-GPG-Key-SHA256" + HeaderPrivatePath = "X-Repository-Private-Key-Path" + HeaderPublicPath = "X-Repository-Public-Key-Path" +) + +type ProbeResponse struct { + Hostname string `json:"hostname"` + IPAddress string `json:"ip_address"` + Architecture string `json:"architecture"` + OSName string `json:"os_name"` + OSVersion string `json:"os_version"` + AgentVersion string `json:"agent_version"` + FilesystemType string `json:"filesystem_type"` + CapacityBytes int64 `json:"capacity_bytes"` + FreeBytes int64 `json:"free_bytes"` + FreeInodes int64 `json:"free_inodes"` + ToolVersions map[string]string `json:"tool_versions"` + ErrorCode string `json:"error_code,omitempty"` + ErrorMessage string `json:"error_message,omitempty"` + ObservedAt time.Time `json:"observed_at"` + RepositoryUsages []RepositoryUsage `json:"repository_usages,omitempty"` + RepositoryUsageSnapshot bool `json:"repository_usage_snapshot"` +} + +type RepositoryUsage struct { + RepositoryID string `json:"repository_id"` + LogicalBytes int64 `json:"logical_bytes"` + PhysicalBytes int64 `json:"physical_bytes"` + PublishStatus string `json:"publish_status"` + ObservedAt time.Time `json:"observed_at"` +} + +type TaskLogMetadata struct { + SchemaVersion int `json:"schema_version"` + TaskID string `json:"task_id"` + RelativePath string `json:"relative_path"` + SHA256 string `json:"sha256"` + CompressedBytes int64 `json:"compressed_bytes"` + UncompressedBytes int64 `json:"uncompressed_bytes"` + LineCount int64 `json:"line_count"` + Summary string `json:"summary"` + ErrorFingerprint string `json:"error_fingerprint,omitempty"` + CompletionStatus string `json:"completion_status"` + ExpiresAt time.Time `json:"expires_at"` + CreatedAt time.Time `json:"created_at"` +} + +type BeginUploadRequest struct { + ExpectedSize int64 `json:"expected_size"` +} + +type BeginUploadResponse struct { + RelativePath string `json:"relative_path"` +} + +type CompleteUploadRequest struct { + ExpectedSize int64 `json:"expected_size"` + ExpectedSHA256 string `json:"expected_sha256,omitempty"` + TotalChunks int `json:"total_chunks,omitempty"` +} + +type PutUploadChunkResponse struct { + RelativePath string `json:"relative_path"` +} + +type CompleteUploadResponse struct { + RelativePath string `json:"relative_path"` + SizeBytes int64 `json:"size_bytes"` + SHA256 string `json:"sha256"` +} + +type GPGKeyResponse struct { + RepositoryID string `json:"repository_id"` + Fingerprint string `json:"fingerprint"` + KeyID string `json:"key_id"` + PrivatePath string `json:"private_path"` + PublicPath string `json:"public_path"` + ContentSHA256 string `json:"content_sha256"` +} + +func TaskResultObjectID(taskID string) (string, error) { + if uuid.Validate(taskID) != nil { + return "", fmt.Errorf("task ID is invalid") + } + return path.Join("task-logs", taskID, "result.jsonl.zst"), nil +} + +func TaskLogRelativePath(taskID string) (string, error) { + if uuid.Validate(taskID) != nil { + return "", fmt.Errorf("task ID is invalid") + } + return path.Join("task-logs", taskID, "execution.jsonl.zst"), nil +} + +func TaskLogMetadataRelativePath(taskID string) (string, error) { + if uuid.Validate(taskID) != nil { + return "", fmt.Errorf("task ID is invalid") + } + return path.Join("task-logs", taskID, "execution.meta.json"), nil +} + +func UploadRelativePath(uploadID string) (string, error) { + if uuid.Validate(uploadID) != nil { + return "", fmt.Errorf("upload ID is invalid") + } + return path.Join("staging", "uploads", uploadID, "content"), nil +} + +func UploadChunkRelativePath(uploadID string, chunkIndex int) (string, error) { + if uuid.Validate(uploadID) != nil || chunkIndex < 0 { + return "", fmt.Errorf("upload chunk identity is invalid") + } + return path.Join("staging", "uploads", uploadID, "chunks", fmt.Sprintf("%08d.part", chunkIndex)), nil +} + +func GPGKeyPath(repositoryID, fingerprint string) (string, error) { + if uuid.Validate(repositoryID) != nil { + return "", fmt.Errorf("repository ID is invalid") + } + if len(fingerprint) != 40 && len(fingerprint) != 64 { + return "", fmt.Errorf("GPG fingerprint is invalid") + } + for _, character := range fingerprint { + if (character < '0' || character > '9') && (character < 'A' || character > 'F') { + return "", fmt.Errorf("GPG fingerprint is invalid") + } + } + return path.Join(GPGKeyPathPrefix, repositoryID, fingerprint), nil +} diff --git a/manager/backend/services/footstone/infrastructure/repository/storage/capacity_linux.go b/manager/backend/services/footstone/infrastructure/repository/storage/capacity_linux.go new file mode 100644 index 0000000000000000000000000000000000000000..78506ebba0217f6326b7b094eeff108d108fcb22 --- /dev/null +++ b/manager/backend/services/footstone/infrastructure/repository/storage/capacity_linux.go @@ -0,0 +1,41 @@ +//go:build linux + +package storage + +import ( + "fmt" + "syscall" +) + +func filesystemFree(path string) (free int64, capacity int64, err error) { + value, err := filesystemCapacity(path) + return value.FreeBytes, value.CapacityBytes, err +} + +func filesystemCapacity(path string) (value Capacity, err error) { + var stat syscall.Statfs_t + if err = syscall.Statfs(path, &stat); err != nil { + return Capacity{}, err + } + value = Capacity{FilesystemType: linuxFilesystemType(int64(stat.Type)), + FreeBytes: int64(stat.Bavail) * int64(stat.Bsize), + CapacityBytes: int64(stat.Blocks) * int64(stat.Bsize), FreeInodes: int64(stat.Ffree)} + return value, nil +} + +func linuxFilesystemType(value int64) string { + switch uint64(value) { + case 0x58465342: + return "xfs" + case 0xef53: + return "ext4" + case 0x9123683e: + return "btrfs" + case 0x794c7630: + return "overlay" + case 0x01021994: + return "tmpfs" + default: + return fmt.Sprintf("fs-%x", uint64(value)) + } +} diff --git a/manager/backend/services/footstone/infrastructure/repository/storage/capacity_other.go b/manager/backend/services/footstone/infrastructure/repository/storage/capacity_other.go new file mode 100644 index 0000000000000000000000000000000000000000..a18a05f539bf29bf7d506ea9839392fc6f0912b6 --- /dev/null +++ b/manager/backend/services/footstone/infrastructure/repository/storage/capacity_other.go @@ -0,0 +1,16 @@ +//go:build !linux + +package storage + +import "math" + +// The production repository runtime is Linux. Other platforms keep builds and +// unit tests portable but do not claim filesystem capacity evidence. +func filesystemFree(string) (free int64, capacity int64, err error) { + return math.MaxInt64, math.MaxInt64, nil +} + +func filesystemCapacity(string) (Capacity, error) { + return Capacity{FilesystemType: "unsupported", CapacityBytes: math.MaxInt64, + FreeBytes: math.MaxInt64, FreeInodes: math.MaxInt64}, nil +} diff --git a/manager/backend/services/footstone/infrastructure/repository/storage/gpg.go b/manager/backend/services/footstone/infrastructure/repository/storage/gpg.go new file mode 100644 index 0000000000000000000000000000000000000000..9349fefec8b5418ad4cd5d21fc667a4bdd84b70c --- /dev/null +++ b/manager/backend/services/footstone/infrastructure/repository/storage/gpg.go @@ -0,0 +1,147 @@ +package storage + +import ( + "fmt" + "os" + "path/filepath" +) + +func (s *Store) GPGKeyRelativePath(repositoryID, fingerprint string) (string, error) { + if err := validatePathComponent("repository ID", repositoryID, 64); err != nil { + return "", err + } + if err := validateFingerprint(fingerprint); err != nil { + return "", err + } + return filepath.ToSlash(filepath.Join("keys", repositoryID, fingerprint, "public.key")), nil +} + +func (s *Store) PublicGPGKeyRelativePath(publishKey, fingerprint string) (string, error) { + if err := validatePathComponent("publish key", publishKey, 64); err != nil { + return "", err + } + if err := validateFingerprint(fingerprint); err != nil { + return "", err + } + return filepath.ToSlash(filepath.Join("public", publishKey, "keys", fingerprint, "public.key")), nil +} + +// PutGPGPublicKey atomically stores the management-only copy and the +// world-readable data-plane copy. Only public OpenPGP material reaches here. +func (s *Store) PutGPGPublicKey(repositoryID, publishKey, fingerprint string, content []byte) (string, error) { + privateRelative, err := s.GPGKeyRelativePath(repositoryID, fingerprint) + if err != nil { + return "", err + } + publicRelative, err := s.PublicGPGKeyRelativePath(publishKey, fingerprint) + if err != nil { + return "", err + } + if err = s.writeAtomic(privateRelative, content, 0640, 0750); err != nil { + return "", err + } + if err = s.writeAtomic(publicRelative, content, 0644, 0755); err != nil { + _ = s.RemoveGPGPublicKey(repositoryID, publishKey, fingerprint) + return "", err + } + return privateRelative, nil +} + +func (s *Store) VerifyGPGPublicKey(repositoryID, publishKey, fingerprint, expectedRelative string) error { + privateRelative, err := s.GPGKeyRelativePath(repositoryID, fingerprint) + if err != nil { + return err + } + if expectedRelative != privateRelative { + return fmt.Errorf("repository GPG key path does not match its metadata") + } + publicRelative, err := s.PublicGPGKeyRelativePath(publishKey, fingerprint) + if err != nil { + return err + } + for _, relative := range []string{privateRelative, publicRelative} { + path, resolveErr := s.Resolve(relative) + if resolveErr != nil { + return resolveErr + } + info, statErr := os.Lstat(path) + if statErr != nil { + return fmt.Errorf("stat repository GPG public key: %w", statErr) + } + if !info.Mode().IsRegular() || info.Size() == 0 { + return fmt.Errorf("repository GPG public key is not a non-empty regular file") + } + } + return nil +} + +func (s *Store) RemoveGPGPublicKey(repositoryID, publishKey, fingerprint string) error { + privateRelative, err := s.GPGKeyRelativePath(repositoryID, fingerprint) + if err != nil { + return err + } + publicRelative, err := s.PublicGPGKeyRelativePath(publishKey, fingerprint) + if err != nil { + return err + } + for _, relative := range []string{privateRelative, publicRelative} { + path, resolveErr := s.Resolve(relative) + if resolveErr != nil { + return resolveErr + } + if removeErr := os.Remove(path); removeErr != nil && !os.IsNotExist(removeErr) { + return removeErr + } + _ = os.Remove(filepath.Dir(path)) + } + return nil +} + +func (s *Store) writeAtomic(relative string, content []byte, mode os.FileMode, directoryMode os.FileMode) error { + if len(content) == 0 { + return fmt.Errorf("repository GPG public key is empty") + } + path, err := s.Resolve(relative) + if err != nil { + return err + } + if err = os.MkdirAll(filepath.Dir(path), directoryMode); err != nil { + return err + } + temporary, err := os.CreateTemp(filepath.Dir(path), ".public-key-*") + if err != nil { + return err + } + temporaryPath := temporary.Name() + defer os.Remove(temporaryPath) + if err = temporary.Chmod(mode); err == nil { + _, err = temporary.Write(content) + } + if err == nil { + err = temporary.Sync() + } + if closeErr := temporary.Close(); err == nil { + err = closeErr + } + if err != nil { + return err + } + if err = os.Rename(temporaryPath, path); err != nil { + return err + } + return os.Chmod(path, mode) +} + +func validateFingerprint(value string) error { + if len(value) != 40 && len(value) != 64 { + return fmt.Errorf("invalid OpenPGP fingerprint") + } + for _, character := range value { + if character < '0' || character > '9' { + if character < 'A' || character > 'F' { + return fmt.Errorf("invalid OpenPGP fingerprint") + } + } + } + return nil +} diff --git a/manager/backend/services/footstone/infrastructure/repository/storage/gpg_test.go b/manager/backend/services/footstone/infrastructure/repository/storage/gpg_test.go new file mode 100644 index 0000000000000000000000000000000000000000..f2861d33f636b96406744b488152f22b1f1a1b38 --- /dev/null +++ b/manager/backend/services/footstone/infrastructure/repository/storage/gpg_test.go @@ -0,0 +1,36 @@ +package storage + +import ( + "os" + "path/filepath" + "testing" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" +) + +func TestPutAndVerifyGPGPublicKey(t *testing.T) { + store, err := New(config.RepositoryConfig{Root: t.TempDir(), PublicBaseURL: "https://repo.example/repositories"}) + if err != nil { + t.Fatal(err) + } + fingerprint := "0123456789ABCDEF0123456789ABCDEF01234567" + relative, err := store.PutGPGPublicKey("repository-id", "publish-key", fingerprint, []byte("public-key")) + if err != nil { + t.Fatal(err) + } + if err = store.VerifyGPGPublicKey("repository-id", "publish-key", fingerprint, relative); err != nil { + t.Fatal(err) + } + url, err := store.PublicGPGKeyURL("publish-key", fingerprint) + if err != nil || url != "https://repo.example/repositories/publish-key/keys/"+fingerprint+"/public.key" { + t.Fatalf("public key URL=%q err=%v", url, err) + } + publicPath, _ := store.Resolve(filepath.ToSlash(filepath.Join("public", "publish-key", "keys", fingerprint, "public.key"))) + info, statErr := os.Stat(publicPath) + if statErr != nil { + t.Fatal(statErr) + } + if info.Mode().Perm() != 0644 { + t.Fatalf("public key mode=%v", info.Mode().Perm()) + } +} diff --git a/manager/backend/services/footstone/infrastructure/repository/storage/publication.go b/manager/backend/services/footstone/infrastructure/repository/storage/publication.go new file mode 100644 index 0000000000000000000000000000000000000000..1959b1db780c37eabcebfb2b169b2ad71888bed5 --- /dev/null +++ b/manager/backend/services/footstone/infrastructure/repository/storage/publication.go @@ -0,0 +1,181 @@ +package storage + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" +) + +var ErrPublishedCurrentMismatch = errors.New("published current revision does not match active revision") + +func (s *Store) PublicRevisionRelativePath(publishKey, revisionID string) (string, error) { + if err := validatePathComponent("publish key", publishKey, 64); err != nil { + return "", err + } + if err := validatePathComponent("revision ID", revisionID, 64); err != nil { + return "", err + } + return filepath.ToSlash(filepath.Join("public", publishKey, "revisions", revisionID)), nil +} + +// VerifyPublishedCurrent confirms that the data-plane link and metadata match +// the active immutable revision before a management-plane .repo is emitted. +func (s *Store) VerifyPublishedCurrent(publishKey, revisionID string) error { + if err := validatePathComponent("publish key", publishKey, 64); err != nil { + return err + } + if err := validatePathComponent("revision ID", revisionID, 64); err != nil { + return err + } + currentRelative := filepath.ToSlash(filepath.Join("public", publishKey, "current")) + current, err := s.Resolve(currentRelative) + if err != nil { + return err + } + info, err := os.Lstat(current) + if err != nil { + return fmt.Errorf("%w: stat current link: %v", ErrPublishedCurrentMismatch, err) + } + if info.Mode()&os.ModeSymlink == 0 { + return fmt.Errorf("%w: current is not a symlink", ErrPublishedCurrentMismatch) + } + target, err := os.Readlink(current) + if err != nil { + return fmt.Errorf("%w: read current link: %v", ErrPublishedCurrentMismatch, err) + } + expected := filepath.ToSlash(filepath.Join("revisions", revisionID)) + if filepath.IsAbs(target) || filepath.ToSlash(target) != expected { + return fmt.Errorf("%w: target=%q expected=%q", ErrPublishedCurrentMismatch, + filepath.ToSlash(target), expected) + } + revisionRelative, err := s.PublicRevisionRelativePath(publishKey, revisionID) + if err != nil { + return err + } + revisionPath, err := s.Resolve(revisionRelative) + if err != nil { + return err + } + canonicalRoot, err := filepath.EvalSymlinks(s.root) + if err != nil { + return fmt.Errorf("%w: resolve repository root: %v", ErrPublishedCurrentMismatch, err) + } + canonicalRevision, err := filepath.EvalSymlinks(revisionPath) + if err != nil { + return fmt.Errorf("%w: resolve active revision: %v", ErrPublishedCurrentMismatch, err) + } + expectedRevision := filepath.Join(canonicalRoot, filepath.FromSlash(revisionRelative)) + if canonicalRevision != expectedRevision { + return fmt.Errorf("%w: active revision escapes repository root", ErrPublishedCurrentMismatch) + } + repomd := filepath.Join(revisionPath, "repodata", "repomd.xml") + repomdInfo, err := os.Stat(repomd) + if err != nil { + return fmt.Errorf("%w: stat repomd.xml: %v", ErrPublishedCurrentMismatch, err) + } + if !repomdInfo.Mode().IsRegular() || repomdInfo.Size() == 0 { + return fmt.Errorf("%w: repomd.xml is not a non-empty regular file", ErrPublishedCurrentMismatch) + } + file, err := os.Open(repomd) + if err != nil { + return fmt.Errorf("%w: open repomd.xml: %v", ErrPublishedCurrentMismatch, err) + } + if err = file.Close(); err != nil { + return fmt.Errorf("%w: close repomd.xml: %v", ErrPublishedCurrentMismatch, err) + } + return nil +} + +// SwitchCurrent atomically replaces current with a relative symlink and returns +// the previous relative target. An empty previous target means no link existed. +func (s *Store) SwitchCurrent(publishKey, revisionID string) (string, error) { + revisionRelative, err := s.PublicRevisionRelativePath(publishKey, revisionID) + if err != nil { + return "", err + } + revisionPath, err := s.Resolve(revisionRelative) + if err != nil { + return "", err + } + info, err := os.Stat(revisionPath) + if err != nil { + return "", fmt.Errorf("stat published revision: %w", err) + } + if !info.IsDir() { + return "", fmt.Errorf("published revision is not a directory") + } + return s.replaceCurrent(publishKey, filepath.ToSlash(filepath.Join("revisions", revisionID))) +} + +func (s *Store) RestoreCurrent(publishKey, previousTarget string) error { + if err := validatePathComponent("publish key", publishKey, 64); err != nil { + return err + } + if previousTarget == "" { + current, err := s.Resolve(filepath.ToSlash(filepath.Join("public", publishKey, "current"))) + if err != nil { + return err + } + if err = os.Remove(current); err != nil && !os.IsNotExist(err) { + return err + } + return nil + } + clean := filepath.ToSlash(filepath.Clean(filepath.FromSlash(previousTarget))) + if clean != previousTarget || !strings.HasPrefix(clean, "revisions/") || + strings.Count(clean, "/") != 1 { + return ErrInvalidRelativePath + } + return s.replaceCurrentTarget(publishKey, clean) +} + +func (s *Store) replaceCurrent(publishKey, target string) (string, error) { + current, err := s.Resolve(filepath.ToSlash(filepath.Join("public", publishKey, "current"))) + if err != nil { + return "", err + } + previous := "" + if value, readErr := os.Readlink(current); readErr == nil { + previous = filepath.ToSlash(value) + } else if !os.IsNotExist(readErr) { + return "", fmt.Errorf("read current repository link: %w", readErr) + } + if err = s.replaceCurrentTarget(publishKey, target); err != nil { + return "", err + } + return previous, nil +} + +func (s *Store) replaceCurrentTarget(publishKey, target string) error { + rootRelative := filepath.ToSlash(filepath.Join("public", publishKey)) + root, err := s.Resolve(rootRelative) + if err != nil { + return err + } + if err = os.MkdirAll(root, 0755); err != nil { + return err + } + current := filepath.Join(root, "current") + temporary := filepath.Join(root, ".current.tmp") + if err = os.Remove(temporary); err != nil && !os.IsNotExist(err) { + return err + } + if err = os.Symlink(filepath.FromSlash(target), temporary); err != nil { + return fmt.Errorf("create current repository link: %w", err) + } + if err = os.Rename(temporary, current); err != nil { + _ = os.Remove(temporary) + return fmt.Errorf("replace current repository link: %w", err) + } + return nil +} + +func validatePathComponent(field, value string, maximum int) error { + if value == "" || len(value) > maximum || value == "." || value == ".." || + strings.ContainsAny(value, `/\\`) { + return fmt.Errorf("invalid %s", field) + } + return nil +} diff --git a/manager/backend/services/footstone/infrastructure/repository/storage/publication_test.go b/manager/backend/services/footstone/infrastructure/repository/storage/publication_test.go new file mode 100644 index 0000000000000000000000000000000000000000..bb2d9f204845e7105060554141b80d69f9ad8f3f --- /dev/null +++ b/manager/backend/services/footstone/infrastructure/repository/storage/publication_test.go @@ -0,0 +1,92 @@ +package storage + +import ( + "errors" + "os" + "path/filepath" + "testing" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" +) + +func TestSwitchAndRestoreCurrentUsesRelativeSymlink(t *testing.T) { + store, err := New(config.RepositoryConfig{Root: t.TempDir(), MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + for _, revision := range []string{"revision-one", "revision-two"} { + relative, pathErr := store.PublicRevisionRelativePath("publish-key", revision) + if pathErr != nil { + t.Fatal(pathErr) + } + path, _ := store.Resolve(relative) + if pathErr = os.MkdirAll(path, 0755); pathErr != nil { + t.Fatal(pathErr) + } + } + previous, err := store.SwitchCurrent("publish-key", "revision-one") + if err != nil || previous != "" { + t.Fatalf("first switch previous=%q err=%v", previous, err) + } + if err = os.MkdirAll(filepath.Join(store.Root(), "public", "publish-key", "revisions", "revision-one", + "repodata"), 0755); err != nil { + t.Fatal(err) + } + if err = os.WriteFile(filepath.Join(store.Root(), "public", "publish-key", "revisions", "revision-one", + "repodata", "repomd.xml"), []byte("repomd-one"), 0444); err != nil { + t.Fatal(err) + } + if err = store.VerifyPublishedCurrent("publish-key", "revision-one"); err != nil { + t.Fatalf("verify first published current: %v", err) + } + previous, err = store.SwitchCurrent("publish-key", "revision-two") + if err != nil || previous != "revisions/revision-one" { + t.Fatalf("second switch previous=%q err=%v", previous, err) + } + if err = store.VerifyPublishedCurrent("publish-key", "revision-two"); !errors.Is(err, ErrPublishedCurrentMismatch) { + t.Fatalf("missing repomd mismatch error=%v", err) + } + current := filepath.Join(store.Root(), "public", "publish-key", "current") + if target, readErr := os.Readlink(current); readErr != nil || filepath.IsAbs(target) || + filepath.ToSlash(target) != "revisions/revision-two" { + t.Fatalf("current target=%q err=%v", target, readErr) + } + if err = store.RestoreCurrent("publish-key", previous); err != nil { + t.Fatal(err) + } + if target, readErr := os.Readlink(current); readErr != nil || + filepath.ToSlash(target) != "revisions/revision-one" { + t.Fatalf("restored target=%q err=%v", target, readErr) + } + if err = store.VerifyPublishedCurrent("publish-key", "revision-two"); !errors.Is(err, ErrPublishedCurrentMismatch) { + t.Fatalf("active revision mismatch error=%v", err) + } +} + +func TestVerifyPublishedCurrentRejectsRevisionParentSymlink(t *testing.T) { + store, err := New(config.RepositoryConfig{Root: t.TempDir(), MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + outside := t.TempDir() + outsideRevision := filepath.Join(outside, "revision-one", "repodata") + if err = os.MkdirAll(outsideRevision, 0755); err != nil { + t.Fatal(err) + } + if err = os.WriteFile(filepath.Join(outsideRevision, "repomd.xml"), []byte("outside"), 0444); err != nil { + t.Fatal(err) + } + publishRoot := filepath.Join(store.Root(), "public", "publish-key") + if err = os.MkdirAll(publishRoot, 0755); err != nil { + t.Fatal(err) + } + if err = os.Symlink(outside, filepath.Join(publishRoot, "revisions")); err != nil { + t.Skipf("symlink is not available: %v", err) + } + if err = os.Symlink(filepath.FromSlash("revisions/revision-one"), filepath.Join(publishRoot, "current")); err != nil { + t.Fatal(err) + } + if err = store.VerifyPublishedCurrent("publish-key", "revision-one"); !errors.Is(err, ErrPublishedCurrentMismatch) { + t.Fatalf("revision parent escape error=%v", err) + } +} diff --git a/manager/backend/services/footstone/infrastructure/repository/storage/storage.go b/manager/backend/services/footstone/infrastructure/repository/storage/storage.go new file mode 100644 index 0000000000000000000000000000000000000000..fca9d73da60a8ebb15611f0760cd7a6ff8a7beb9 --- /dev/null +++ b/manager/backend/services/footstone/infrastructure/repository/storage/storage.go @@ -0,0 +1,423 @@ +package storage + +import ( + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "io" + "math" + "net/url" + "os" + "path/filepath" + "strings" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" +) + +var ( + ErrInvalidRelativePath = errors.New("invalid repository relative path") + ErrOffsetMismatch = errors.New("upload offset does not match stored size") + ErrChunkConflict = errors.New("upload chunk already exists with different content") + ErrInsufficientSpace = errors.New("insufficient repository storage space") + ErrPublicBaseURLUnset = errors.New("repository public base URL is not configured") +) + +type Store struct { + root string + cfg config.RepositoryConfig +} + +type Capacity struct { + FilesystemType string + CapacityBytes int64 + FreeBytes int64 + FreeInodes int64 +} + +func New(cfg config.RepositoryConfig) (*Store, error) { + cfg = cfg.WithDefaults() + publicBaseURL, err := normalizePublicBaseURL(cfg.PublicBaseURL) + if err != nil { + return nil, err + } + cfg.PublicBaseURL = publicBaseURL + root, err := filepath.Abs(cfg.Root) + if err != nil { + return nil, fmt.Errorf("resolve repository root: %w", err) + } + s := &Store{root: filepath.Clean(root), cfg: cfg} + for _, dir := range []string{"staging/uploads", "staging/sync", "staging/publish", "quarantine", + "revisions-private", "task-logs", "keys", "trash", "public"} { + if err := os.MkdirAll(filepath.Join(s.root, filepath.FromSlash(dir)), 0750); err != nil { + return nil, fmt.Errorf("create repository directory %s: %w", dir, err) + } + } + if err := os.Chmod(filepath.Join(s.root, "public"), 0755); err != nil { + return nil, fmt.Errorf("set repository public directory permissions: %w", err) + } + return s, nil +} + +func (s *Store) Root() string { return s.root } + +func (s *Store) Config() config.RepositoryConfig { return s.cfg } + +func (s *Store) Capacity() (Capacity, error) { + return filesystemCapacity(s.root) +} + +func (s *Store) PublicCurrentURL(publishKey string) (string, error) { + return s.publicURL([]string{publishKey, "current"}) +} + +func (s *Store) PublicRevisionURL(publishKey, revisionID string) (string, error) { + return s.publicURL([]string{publishKey, "revisions", revisionID}) +} + +func (s *Store) PublicGPGKeyURL(publishKey, fingerprint string) (string, error) { + return s.publicFileURL([]string{publishKey, "keys", fingerprint, "public.key"}) +} + +func (s *Store) publicURL(components []string) (string, error) { + return s.buildPublicURL(components, true) +} + +func (s *Store) publicFileURL(components []string) (string, error) { + return s.buildPublicURL(components, false) +} + +func (s *Store) buildPublicURL(components []string, directory bool) (string, error) { + if s.cfg.PublicBaseURL == "" { + return "", ErrPublicBaseURLUnset + } + for index, component := range components { + if err := validatePathComponent("public URL component", component, 64); err != nil { + return "", fmt.Errorf("component %d: %w", index, err) + } + } + value, err := url.Parse(s.cfg.PublicBaseURL) + if err != nil { + return "", fmt.Errorf("parse repository public base URL: %w", err) + } + value.Path = strings.TrimRight(value.Path, "/") + "/" + strings.Join(components, "/") + if directory { + value.Path += "/" + } + value.RawPath = "" + return value.String(), nil +} + +func normalizePublicBaseURL(value string) (string, error) { + value = strings.TrimSpace(value) + if value == "" { + return "", nil + } + if strings.ContainsAny(value, "\r\n\t") { + return "", fmt.Errorf("repository public base URL contains control characters") + } + parsed, err := url.Parse(value) + if err != nil { + return "", fmt.Errorf("parse repository public base URL: %w", err) + } + if (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" || + parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" || parsed.Opaque != "" { + return "", fmt.Errorf("repository public base URL must be an absolute HTTP(S) URL without credentials, query, or fragment") + } + parsed.Path = strings.TrimRight(parsed.Path, "/") + parsed.RawPath = "" + return strings.TrimRight(parsed.String(), "/"), nil +} + +func (s *Store) UploadRelativePath(uploadID string) string { + return filepath.ToSlash(filepath.Join("staging", "uploads", uploadID, "content")) +} + +func (s *Store) UploadChunkRelativePath(uploadID string, chunkIndex int) string { + return filepath.ToSlash(filepath.Join("staging", "uploads", uploadID, "chunks", + fmt.Sprintf("%08d.part", chunkIndex))) +} + +func (s *Store) CreateUpload(uploadID string, expectedSize int64) (string, error) { + if err := s.EnsureFreeSpace(expectedSize); err != nil { + return "", err + } + relative := s.UploadRelativePath(uploadID) + path, err := s.Resolve(relative) + if err != nil { + return "", err + } + if err = os.MkdirAll(filepath.Dir(path), 0700); err != nil { + return "", fmt.Errorf("create upload directory: %w", err) + } + file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) + if err != nil { + return "", fmt.Errorf("create upload file: %w", err) + } + return relative, file.Close() +} + +func (s *Store) Append(relative string, offset int64, data []byte, expectedSize int64) (int64, error) { + if len(data) == 0 || len(data) > s.cfg.MaxChunkBytes { + return 0, fmt.Errorf("chunk size must be between 1 and %d bytes", s.cfg.MaxChunkBytes) + } + path, err := s.Resolve(relative) + if err != nil { + return 0, err + } + file, err := os.OpenFile(path, os.O_WRONLY, 0600) + if err != nil { + return 0, fmt.Errorf("open upload file: %w", err) + } + defer file.Close() + info, err := file.Stat() + if err != nil { + return 0, fmt.Errorf("stat upload file: %w", err) + } + if !info.Mode().IsRegular() || info.Size() != offset { + return info.Size(), ErrOffsetMismatch + } + if offset+int64(len(data)) > expectedSize { + return info.Size(), fmt.Errorf("chunk exceeds declared upload size") + } + if err = s.EnsureFreeSpace(int64(len(data))); err != nil { + return info.Size(), err + } + written, err := file.WriteAt(data, offset) + if err != nil { + return info.Size(), fmt.Errorf("write upload chunk: %w", err) + } + if written != len(data) { + return info.Size() + int64(written), io.ErrShortWrite + } + if err = file.Sync(); err != nil { + return offset + int64(written), fmt.Errorf("sync upload chunk: %w", err) + } + return offset + int64(written), nil +} + +// PutUploadChunk stores one independently addressable chunk. Repeating the +// same index and digest is idempotent; a different body for that index is +// rejected so a resumed client cannot silently alter an existing session. +func (s *Store) PutUploadChunk(uploadID string, chunkIndex int, data []byte, expectedSHA256 string) (string, error) { + if chunkIndex < 0 || len(data) == 0 || len(data) > s.cfg.MaxChunkBytes { + return "", fmt.Errorf("chunk metadata is invalid") + } + digest := sha256.Sum256(data) + digestHex := hex.EncodeToString(digest[:]) + if digestHex != expectedSHA256 { + return "", fmt.Errorf("upload chunk checksum mismatch") + } + relative := s.UploadChunkRelativePath(uploadID, chunkIndex) + path, err := s.Resolve(relative) + if err != nil { + return "", err + } + if existingDigest, _, hashErr := s.SHA256(relative); hashErr == nil { + if existingDigest == expectedSHA256 { + return relative, nil + } + return "", ErrChunkConflict + } else if !errors.Is(hashErr, os.ErrNotExist) { + return "", hashErr + } + if err = s.EnsureFreeSpace(int64(len(data))); err != nil { + return "", err + } + if err = os.MkdirAll(filepath.Dir(path), 0700); err != nil { + return "", fmt.Errorf("create upload chunk directory: %w", err) + } + temporary, err := os.CreateTemp(filepath.Dir(path), ".chunk-*") + if err != nil { + return "", fmt.Errorf("create upload chunk: %w", err) + } + temporaryName := temporary.Name() + defer os.Remove(temporaryName) + if err = temporary.Chmod(0600); err == nil { + _, err = temporary.Write(data) + } + if err == nil { + err = temporary.Sync() + } + if closeErr := temporary.Close(); err == nil { + err = closeErr + } + if err != nil { + return "", fmt.Errorf("write upload chunk: %w", err) + } + if err = os.Rename(temporaryName, path); err != nil { + if existingDigest, _, hashErr := s.SHA256(relative); hashErr == nil && existingDigest == expectedSHA256 { + return relative, nil + } + return "", fmt.Errorf("publish upload chunk: %w", err) + } + return relative, nil +} + +// AssembleUpload materializes indexed chunks into the legacy content path so +// the established validation and worker pipeline remains unchanged. +func (s *Store) AssembleUpload(uploadID string, totalChunks int, expectedSize int64) (string, int64, string, error) { + if totalChunks <= 0 || expectedSize <= 0 { + return "", 0, "", fmt.Errorf("upload assembly metadata is invalid") + } + relative := s.UploadRelativePath(uploadID) + contentPath, err := s.Resolve(relative) + if err != nil { + return "", 0, "", err + } + temporary, err := os.CreateTemp(filepath.Dir(contentPath), ".content-*") + if err != nil { + return "", 0, "", fmt.Errorf("create assembled upload: %w", err) + } + temporaryName := temporary.Name() + defer os.Remove(temporaryName) + hash := sha256.New() + writer := io.MultiWriter(temporary, hash) + var size int64 + for index := 0; index < totalChunks; index++ { + chunkPath, resolveErr := s.Resolve(s.UploadChunkRelativePath(uploadID, index)) + if resolveErr != nil { + temporary.Close() + return "", 0, "", resolveErr + } + chunk, openErr := os.Open(chunkPath) + if openErr != nil { + temporary.Close() + return "", 0, "", fmt.Errorf("open upload chunk %d: %w", index, openErr) + } + written, copyErr := io.Copy(writer, chunk) + closeErr := chunk.Close() + if copyErr != nil { + temporary.Close() + return "", 0, "", fmt.Errorf("assemble upload chunk %d: %w", index, copyErr) + } + if closeErr != nil { + temporary.Close() + return "", 0, "", closeErr + } + size += written + if size > expectedSize { + temporary.Close() + return "", size, "", fmt.Errorf("assembled upload exceeds declared size") + } + } + if size != expectedSize { + temporary.Close() + return "", size, "", fmt.Errorf("assembled upload is incomplete") + } + if err = temporary.Sync(); err == nil { + err = temporary.Close() + } + if err != nil { + return "", 0, "", fmt.Errorf("sync assembled upload: %w", err) + } + // The content object is staging-only until Complete returns, so replacing + // the empty legacy placeholder does not affect any published revision. + if err = os.Remove(contentPath); err != nil && !errors.Is(err, os.ErrNotExist) { + return "", 0, "", fmt.Errorf("replace assembled upload: %w", err) + } + if err = os.Rename(temporaryName, contentPath); err != nil { + return "", 0, "", fmt.Errorf("publish assembled upload: %w", err) + } + return relative, size, hex.EncodeToString(hash.Sum(nil)), nil +} + +func (s *Store) SHA256(relative string) (string, int64, error) { + path, err := s.Resolve(relative) + if err != nil { + return "", 0, err + } + file, err := os.Open(path) + if err != nil { + return "", 0, err + } + defer file.Close() + hash := sha256.New() + size, err := io.Copy(hash, file) + if err != nil { + return "", 0, err + } + return hex.EncodeToString(hash.Sum(nil)), size, nil +} + +func (s *Store) Size(relative string) (int64, error) { + path, err := s.Resolve(relative) + if err != nil { + return 0, err + } + info, err := os.Lstat(path) + if err != nil { + return 0, err + } + if !info.Mode().IsRegular() { + return 0, fmt.Errorf("repository path is not a regular file") + } + return info.Size(), nil +} + +func (s *Store) MoveToTrash(relative, category, id string) (string, error) { + source, err := s.Resolve(relative) + if err != nil { + return "", err + } + destinationRelative := filepath.ToSlash(filepath.Join("trash", category, id)) + destination, err := s.Resolve(destinationRelative) + if err != nil { + return "", err + } + if err = os.MkdirAll(filepath.Dir(destination), 0750); err != nil { + return "", err + } + if err = os.Rename(filepath.Dir(source), destination); err != nil { + return "", fmt.Errorf("move repository data to trash: %w", err) + } + return destinationRelative, nil +} + +func (s *Store) Resolve(relative string) (string, error) { + if relative == "" || filepath.IsAbs(relative) { + return "", ErrInvalidRelativePath + } + clean := filepath.Clean(filepath.FromSlash(relative)) + if clean == "." || clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) { + return "", ErrInvalidRelativePath + } + resolved := filepath.Join(s.root, clean) + prefix := s.root + string(filepath.Separator) + if resolved != s.root && !strings.HasPrefix(resolved, prefix) { + return "", ErrInvalidRelativePath + } + return resolved, nil +} + +func (s *Store) EnsureFreeSpace(required int64) error { + free, capacity, err := filesystemFree(s.root) + if err != nil { + return fmt.Errorf("stat repository filesystem: %w", err) + } + threshold, ok := safeCapacitySum(required, s.cfg.MinimumFreeBytes) + if !ok || !capacityAvailable(free, capacity, threshold) { + return fmt.Errorf("%w: free=%d required=%d", ErrInsufficientSpace, free, threshold) + } + return nil +} + +func safeCapacitySum(left, right int64) (int64, bool) { + if left < 0 || right < 0 || left > math.MaxInt64-right { + return math.MaxInt64, false + } + return left + right, true +} + +// capacityAvailable applies the 90% hard-usage threshold to the projected +// post-write state, not only to the current filesystem snapshot. +func capacityAvailable(free, capacity, required int64) bool { + if free < 0 || capacity <= 0 || required < 0 || free < required { + return false + } + minimumRemaining := capacity / 10 + if capacity%10 != 0 { + minimumRemaining++ + } + return free-required >= minimumRemaining +} diff --git a/manager/backend/services/footstone/infrastructure/repository/storage/storage_test.go b/manager/backend/services/footstone/infrastructure/repository/storage/storage_test.go new file mode 100644 index 0000000000000000000000000000000000000000..e1535d17ef0350e1b1878789686fb388bb40773e --- /dev/null +++ b/manager/backend/services/footstone/infrastructure/repository/storage/storage_test.go @@ -0,0 +1,173 @@ +package storage + +import ( + "crypto/sha256" + "encoding/hex" + "errors" + "os" + "path/filepath" + "strings" + "testing" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" +) + +func TestIndexedUploadChunksAreIdempotentAndAssembledInOrder(t *testing.T) { + store, err := New(config.RepositoryConfig{Root: t.TempDir(), MinimumFreeBytes: 1, + MaxChunkBytes: 4, MaxUploadBytes: 16}) + if err != nil { + t.Fatal(err) + } + if _, err = store.CreateUpload("upload-indexed", 6); err != nil { + t.Fatal(err) + } + second := []byte("def") + secondDigest := sha256.Sum256(second) + secondSHA := hex.EncodeToString(secondDigest[:]) + if _, err = store.PutUploadChunk("upload-indexed", 1, second, secondSHA); err != nil { + t.Fatal(err) + } + first := []byte("abc") + firstDigest := sha256.Sum256(first) + firstSHA := hex.EncodeToString(firstDigest[:]) + if _, err = store.PutUploadChunk("upload-indexed", 0, first, firstSHA); err != nil { + t.Fatal(err) + } + if _, err = store.PutUploadChunk("upload-indexed", 0, first, firstSHA); err != nil { + t.Fatalf("idempotent chunk retry failed: %v", err) + } + conflict := []byte("xyz") + conflictDigest := sha256.Sum256(conflict) + if _, err = store.PutUploadChunk("upload-indexed", 0, conflict, + hex.EncodeToString(conflictDigest[:])); !errors.Is(err, ErrChunkConflict) { + t.Fatalf("conflicting chunk error = %v", err) + } + relative, size, digest, err := store.AssembleUpload("upload-indexed", 2, 6) + if err != nil || size != 6 || digest != "bef57ec7f53a6d40beb640a780a639c83bc29ac8a9816f1fc6c5c6dcd93c4721" { + t.Fatalf("assembled relative/size/digest/error = %q/%d/%q/%v", relative, size, digest, err) + } + content, err := os.ReadFile(filepath.Join(store.Root(), filepath.FromSlash(relative))) + if err != nil || string(content) != "abcdef" { + t.Fatalf("assembled content/error = %q/%v", content, err) + } +} + +func TestUploadLifecycleAndContainment(t *testing.T) { + store, err := New(config.RepositoryConfig{Root: t.TempDir(), MaxChunkBytes: 4, MaxUploadBytes: 16}) + if err != nil { + t.Fatal(err) + } + relative, err := store.CreateUpload("upload-a", 6) + if err != nil { + t.Fatal(err) + } + if next, err := store.Append(relative, 0, []byte("abc"), 6); err != nil || next != 3 { + t.Fatalf("first append next=%d err=%v", next, err) + } + if _, err := store.Append(relative, 0, []byte("x"), 6); !errors.Is(err, ErrOffsetMismatch) { + t.Fatalf("offset mismatch error=%v", err) + } + if next, err := store.Append(relative, 3, []byte("def"), 6); err != nil || next != 6 { + t.Fatalf("second append next=%d err=%v", next, err) + } + digest, size, err := store.SHA256(relative) + if err != nil || size != 6 || digest != "bef57ec7f53a6d40beb640a780a639c83bc29ac8a9816f1fc6c5c6dcd93c4721" { + t.Fatalf("sha256=%s size=%d err=%v", digest, size, err) + } + if _, err = store.Resolve("../escape"); !errors.Is(err, ErrInvalidRelativePath) { + t.Fatalf("traversal error=%v", err) + } + resolved, err := store.Resolve(relative) + if err != nil { + t.Fatal(err) + } + if info, err := os.Stat(resolved); err != nil || info.Size() != 6 { + t.Fatalf("stored file info=%v err=%v", info, err) + } + if !filepath.IsAbs(store.Root()) { + t.Fatalf("root is not absolute: %s", store.Root()) + } +} + +func TestPublicRepositoryURLs(t *testing.T) { + store, err := New(config.RepositoryConfig{ + Root: t.TempDir(), PublicBaseURL: "https://repo.example.test/ocmanager/repositories/", + }) + if err != nil { + t.Fatal(err) + } + current, err := store.PublicCurrentURL("publish-key") + if err != nil || current != "https://repo.example.test/ocmanager/repositories/publish-key/current/" { + t.Fatalf("current URL=%q err=%v", current, err) + } + revision, err := store.PublicRevisionURL("publish-key", "revision-id") + if err != nil || revision != "https://repo.example.test/ocmanager/repositories/publish-key/revisions/revision-id/" { + t.Fatalf("revision URL=%q err=%v", revision, err) + } +} + +func TestPublicRepositoryURLValidation(t *testing.T) { + for _, value := range []string{ + "ftp://repo.example.test/repositories", + "https://user:secret@repo.example.test/repositories", + "https://repo.example.test/repositories?token=secret", + "https://repo.example.test/repositories#fragment", + "https://repo.example.test/repositories\nbaseurl=https://attacker.invalid", + } { + _, err := New(config.RepositoryConfig{Root: t.TempDir(), PublicBaseURL: value}) + if err == nil || !strings.Contains(err.Error(), "repository public base URL") { + t.Fatalf("invalid public URL %q error=%v", value, err) + } + } + store, err := New(config.RepositoryConfig{Root: t.TempDir()}) + if err != nil { + t.Fatal(err) + } + if _, err = store.PublicCurrentURL("publish-key"); !errors.Is(err, ErrPublicBaseURLUnset) { + t.Fatalf("unset public URL error=%v", err) + } +} + +func TestUploadLimitsAndTrash(t *testing.T) { + store, err := New(config.RepositoryConfig{Root: t.TempDir(), MaxChunkBytes: 2, MaxUploadBytes: 8}) + if err != nil { + t.Fatal(err) + } + relative, err := store.CreateUpload("upload-b", 4) + if err != nil { + t.Fatal(err) + } + if _, err = store.Append(relative, 0, []byte("abc"), 4); err == nil { + t.Fatal("oversized chunk was accepted") + } + trash, err := store.MoveToTrash(relative, "aborted-uploads", "upload-b") + if err != nil { + t.Fatal(err) + } + path, _ := store.Resolve(trash) + if _, err = os.Stat(path); err != nil { + t.Fatalf("trash path missing: %v", err) + } +} + +func TestCapacityAvailableUsesProjectedNinetyPercentThreshold(t *testing.T) { + tests := []struct { + name string + free, capacity, required int64 + want bool + }{ + {name: "within threshold", free: 500, capacity: 1000, required: 300, want: true}, + {name: "would cross threshold", free: 110, capacity: 1000, required: 11, want: false}, + {name: "exact threshold", free: 110, capacity: 1000, required: 10, want: true}, + {name: "insufficient bytes", free: 99, capacity: 1000, required: 100, want: false}, + {name: "invalid capacity", free: 100, capacity: 0, required: 1, want: false}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := capacityAvailable(test.free, test.capacity, test.required); got != test.want { + t.Fatalf("capacityAvailable(%d, %d, %d) = %v, want %v", + test.free, test.capacity, test.required, got, test.want) + } + }) + } +} diff --git a/manager/backend/services/footstone/infrastructure/repository/tasklog/large_log_integration_test.go b/manager/backend/services/footstone/infrastructure/repository/tasklog/large_log_integration_test.go new file mode 100644 index 0000000000000000000000000000000000000000..7f14d936aa45fc1a00b40397c600ef7c8df993af --- /dev/null +++ b/manager/backend/services/footstone/infrastructure/repository/tasklog/large_log_integration_test.go @@ -0,0 +1,69 @@ +package tasklog + +import ( + "context" + "os" + "strconv" + "strings" + "testing" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" +) + +// TestTaskLogLargeStream is opt-in because the acceptance run writes at least +// one GiB of uncompressed JSONL. It verifies that only bounded summary state is +// retained while the full stream is compressed directly to repository storage. +func TestTaskLogLargeStream(t *testing.T) { + targetText := os.Getenv("OCM_REPOSITORY_LARGE_LOG_BYTES") + if targetText == "" { + t.Skip("OCM_REPOSITORY_LARGE_LOG_BYTES is not set") + } + target, err := strconv.ParseInt(targetText, 10, 64) + if err != nil || target < 1<<20 { + t.Fatalf("OCM_REPOSITORY_LARGE_LOG_BYTES must be at least 1048576: %q", targetText) + } + root := os.Getenv("OCM_REPOSITORY_LARGE_LOG_ROOT") + if root == "" { + root = t.TempDir() + } else { + if _, statErr := os.Stat(root); !os.IsNotExist(statErr) { + t.Fatalf("OCM_REPOSITORY_LARGE_LOG_ROOT must not already exist: %q", root) + } + if err = os.MkdirAll(root, 0700); err != nil { + t.Fatal(err) + } + } + store, err := storage.New(config.RepositoryConfig{Root: root, MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + writer, err := New(store, "22222222-2222-4222-8222-222222222222") + if err != nil { + t.Fatal(err) + } + ctx := WithWriter(context.Background(), writer) + message := strings.Repeat("repository-log-fixture-", 400) + for writer.uncompressed.count < target { + if err = Event(ctx, "INFO", "remote-sync", message, + map[string]string{"authorization": "Bearer should-not-survive"}); err != nil { + t.Fatal(err) + } + } + metadata, err := writer.Close("SUCCESS", "", "") + if err != nil { + t.Fatal(err) + } + if metadata.UncompressedBytes < target || metadata.CompressedBytes <= 0 || + len(metadata.Summary) > MaximumSummaryBytes || strings.Contains(metadata.Summary, "should-not-survive") { + t.Fatalf("large log metadata = %#v", metadata) + } + if metadata.LineCount < 100 || len(writer.first) > 8 || len(writer.errors) > 32 || len(writer.last) > 8 { + t.Fatalf("large log retained state: lines=%d first=%d errors=%d last=%d", + metadata.LineCount, len(writer.first), len(writer.errors), len(writer.last)) + } + t.Logf("large_log uncompressed=%d compressed=%d lines=%d summary=%d sha256=%s", + metadata.UncompressedBytes, metadata.CompressedBytes, metadata.LineCount, + len(metadata.Summary), metadata.SHA256) + t.Logf("large_log_root=%s", store.Root()) +} diff --git a/manager/backend/services/footstone/infrastructure/repository/tasklog/tasklog.go b/manager/backend/services/footstone/infrastructure/repository/tasklog/tasklog.go new file mode 100644 index 0000000000000000000000000000000000000000..b9ec91f22ece593be95aced60c8341aeb83308ad --- /dev/null +++ b/manager/backend/services/footstone/infrastructure/repository/tasklog/tasklog.go @@ -0,0 +1,418 @@ +package tasklog + +import ( + "bufio" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "hash" + "io" + "os" + "path/filepath" + "regexp" + "strings" + "sync" + "time" + "unicode/utf8" + + "github.com/google/uuid" + "github.com/klauspost/compress/zstd" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" +) + +const ( + SchemaVersion = 1 + MaximumSummaryBytes = 64 << 10 + defaultMessageBytes = 8 << 10 + SuccessRetention = 14 * 24 * time.Hour + FailureRetention = 30 * 24 * time.Hour +) + +var ( + credentialURLPattern = regexp.MustCompile(`(?i)([a-z][a-z0-9+.-]*://)([^/@\s:]+):([^/@\s]+)@`) + secretPattern = regexp.MustCompile(`(?i)(authorization|proxy-authorization|password|passwd|token|secret|credential|private[_-]?key|api[_-]?key)(\s*[:=]\s*)([^\s,;]+)`) + secretJSONPattern = regexp.MustCompile(`(?i)("[^"]*(?:authorization|proxy-authorization|password|passwd|token|secret|credential|private[_-]?key|api[_-]?key)[^"]*"\s*:\s*")([^"]*)(")`) + bearerPattern = regexp.MustCompile(`(?i)\b(bearer|basic)\s+[a-z0-9._~+/=-]+`) + privateKeyPattern = regexp.MustCompile(`(?s)-----BEGIN PGP PRIVATE KEY BLOCK-----.*?-----END PGP PRIVATE KEY BLOCK-----`) + pemPrivateKeyPattern = regexp.MustCompile(`(?s)-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----.*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----`) + escapedPEMKeyPattern = regexp.MustCompile(`(?i)-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----.*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----`) +) + +type EventRecord struct { + SchemaVersion int `json:"schema_version"` + Timestamp time.Time `json:"timestamp"` + Level string `json:"level"` + Step string `json:"step"` + Message string `json:"message"` + Fields map[string]string `json:"fields,omitempty"` +} + +type Metadata struct { + SchemaVersion int `json:"schema_version"` + TaskID string `json:"task_id"` + RelativePath string `json:"relative_path"` + SHA256 string `json:"sha256"` + CompressedBytes int64 `json:"compressed_bytes"` + UncompressedBytes int64 `json:"uncompressed_bytes"` + LineCount int64 `json:"line_count"` + Summary string `json:"summary"` + ErrorFingerprint string `json:"error_fingerprint,omitempty"` + CompletionStatus string `json:"completion_status"` + ExpiresAt time.Time `json:"expires_at"` + CreatedAt time.Time `json:"created_at"` +} + +type contextKey struct{} + +type Writer struct { + mu sync.Mutex + store *storage.Store + taskID string + relative string + file *os.File + hash hash.Hash + compressed *countingWriter + uncompressed *countingWriter + zstd *zstd.Encoder + buffer *bufio.Writer + encoder *json.Encoder + lineCount int64 + first []string + errors []string + last []string + closed bool + now func() time.Time +} + +type countingWriter struct { + writer io.Writer + count int64 +} + +func (w *countingWriter) Write(value []byte) (int, error) { + n, err := w.writer.Write(value) + w.count += int64(n) + return n, err +} + +func New(store *storage.Store, taskID string) (*Writer, error) { + if store == nil || uuid.Validate(taskID) != nil { + return nil, errors.New("repository task log requires storage and a valid task ID") + } + relative := filepath.ToSlash(filepath.Join("task-logs", taskID, "execution.jsonl.zst")) + path, err := store.Resolve(relative) + if err != nil { + return nil, err + } + if err = os.MkdirAll(filepath.Dir(path), 0700); err != nil { + return nil, fmt.Errorf("create repository task log directory: %w", err) + } + if err = archivePreviousAttempt(store, taskID, path); err != nil { + return nil, err + } + file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) + if err != nil { + return nil, fmt.Errorf("create repository task log: %w", err) + } + digest := sha256.New() + compressed := &countingWriter{writer: io.MultiWriter(file, digest)} + zstdWriter, err := zstd.NewWriter(compressed, + zstd.WithEncoderLevel(zstd.EncoderLevelFromZstd(3)), zstd.WithEncoderCRC(true)) + if err != nil { + _ = file.Close() + _ = os.Remove(path) + return nil, err + } + buffer := bufio.NewWriterSize(zstdWriter, 64<<10) + uncompressed := &countingWriter{writer: buffer} + return &Writer{store: store, taskID: taskID, relative: relative, file: file, hash: digest, + compressed: compressed, uncompressed: uncompressed, zstd: zstdWriter, buffer: buffer, + encoder: json.NewEncoder(uncompressed), now: func() time.Time { return time.Now().UTC() }}, nil +} + +// A task keeps its identity when a lease is recovered. Preserve the completed +// local attempt before the replacement worker opens the active log path again. +func archivePreviousAttempt(store *storage.Store, taskID, logPath string) error { + if _, err := os.Stat(logPath); errors.Is(err, os.ErrNotExist) { + return nil + } else if err != nil { + return fmt.Errorf("inspect previous repository task log: %w", err) + } + recoveryDirectory := filepath.Join(filepath.Dir(logPath), "recovered") + if err := os.MkdirAll(recoveryDirectory, 0700); err != nil { + return fmt.Errorf("create recovered repository task log directory: %w", err) + } + attemptID := uuid.NewString() + archivedLog := filepath.Join(recoveryDirectory, "execution-"+attemptID+".jsonl.zst") + if err := os.Rename(logPath, archivedLog); err != nil { + return fmt.Errorf("archive previous repository task log: %w", err) + } + metadataPath, err := store.Resolve(filepath.ToSlash(filepath.Join("task-logs", taskID, "execution.meta.json"))) + if err != nil { + return err + } + archivedMetadata := filepath.Join(recoveryDirectory, "execution-"+attemptID+".meta.json") + if err = os.Rename(metadataPath, archivedMetadata); err != nil && !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("archive previous repository task log metadata: %w", err) + } + return nil +} + +func WithWriter(ctx context.Context, writer *Writer) context.Context { + if writer == nil { + return ctx + } + return context.WithValue(ctx, contextKey{}, writer) +} + +func Event(ctx context.Context, level, step, message string, fields map[string]string) error { + writer, _ := ctx.Value(contextKey{}).(*Writer) + if writer == nil { + return nil + } + return writer.Event(level, step, message, fields) +} + +func (w *Writer) Event(level, step, message string, fields map[string]string) error { + w.mu.Lock() + defer w.mu.Unlock() + if w.closed { + return errors.New("repository task log is closed") + } + level = strings.ToUpper(strings.TrimSpace(level)) + switch level { + case "DEBUG", "INFO", "WARN", "ERROR": + default: + level = "INFO" + } + step, message = bounded(Redact(step), 128), bounded(Redact(message), defaultMessageBytes) + cleanFields := make(map[string]string, len(fields)) + for key, value := range fields { + key = bounded(Redact(key), 64) + if key != "" { + cleanFields[key] = bounded(Redact(value), 2048) + } + } + record := EventRecord{SchemaVersion: SchemaVersion, Timestamp: w.now(), Level: level, + Step: step, Message: message, Fields: cleanFields} + if err := w.encoder.Encode(record); err != nil { + return err + } + w.lineCount++ + summaryLine := fmt.Sprintf("%s %-5s %s: %s", record.Timestamp.Format(time.RFC3339), level, step, message) + w.addSummary(level, summaryLine) + return nil +} + +func (w *Writer) Close(completionStatus, errorCode, errorMessage string) (*Metadata, error) { + w.mu.Lock() + defer w.mu.Unlock() + if w.closed { + return nil, errors.New("repository task log is already closed") + } + w.closed = true + var closeErr error + if err := w.buffer.Flush(); err != nil { + closeErr = err + } + if err := w.zstd.Close(); closeErr == nil && err != nil { + closeErr = err + } + if err := w.file.Sync(); closeErr == nil && err != nil { + closeErr = err + } + if err := w.file.Close(); closeErr == nil && err != nil { + closeErr = err + } + if closeErr != nil { + return nil, closeErr + } + now := w.now() + retention := SuccessRetention + if completionStatus != "SUCCESS" { + retention = FailureRetention + } + metadata := &Metadata{SchemaVersion: SchemaVersion, TaskID: w.taskID, RelativePath: w.relative, + SHA256: hex.EncodeToString(w.hash.Sum(nil)), CompressedBytes: w.compressed.count, + UncompressedBytes: w.uncompressed.count, LineCount: w.lineCount, + Summary: w.summary(), CompletionStatus: completionStatus, ExpiresAt: now.Add(retention), CreatedAt: now} + if errorCode != "" || errorMessage != "" { + fingerprint := sha256.Sum256([]byte(strings.ToUpper(strings.TrimSpace(errorCode)) + "\n" + + strings.ToLower(strings.Join(strings.Fields(Redact(errorMessage)), " ")))) + metadata.ErrorFingerprint = hex.EncodeToString(fingerprint[:]) + } + if err := writeMetadata(w.store, metadata); err != nil { + return nil, err + } + return metadata, nil +} + +func writeMetadata(store *storage.Store, metadata *Metadata) error { + relative := filepath.ToSlash(filepath.Join("task-logs", metadata.TaskID, "execution.meta.json")) + path, err := store.Resolve(relative) + if err != nil { + return err + } + temporary := path + ".tmp" + content, err := json.Marshal(metadata) + if err != nil { + return err + } + content = append(content, '\n') + if err = os.WriteFile(temporary, content, 0600); err != nil { + return err + } + if err = os.Rename(temporary, path); err != nil { + _ = os.Remove(temporary) + return err + } + return nil +} + +func ReadMetadata(store *storage.Store, taskID string) (*Metadata, error) { + if store == nil || uuid.Validate(taskID) != nil { + return nil, errors.New("repository task log metadata identity is invalid") + } + relative := filepath.ToSlash(filepath.Join("task-logs", taskID, "execution.meta.json")) + path, err := store.Resolve(relative) + if err != nil { + return nil, err + } + content, err := os.ReadFile(path) + if err != nil { + return nil, err + } + if len(content) > 128<<10 { + return nil, errors.New("repository task log metadata exceeds its size limit") + } + value := new(Metadata) + decoder := json.NewDecoder(strings.NewReader(string(content))) + decoder.DisallowUnknownFields() + if err = decoder.Decode(value); err != nil || decoder.Decode(new(interface{})) != io.EOF { + return nil, errors.New("repository task log metadata is invalid") + } + if value.SchemaVersion != SchemaVersion || value.TaskID != taskID || + value.RelativePath != filepath.ToSlash(filepath.Join("task-logs", taskID, "execution.jsonl.zst")) || + len(value.SHA256) != 64 || value.CompressedBytes <= 0 || value.UncompressedBytes <= 0 || + value.LineCount <= 0 || len(value.Summary) > MaximumSummaryBytes || value.ExpiresAt.IsZero() { + return nil, errors.New("repository task log metadata contract is invalid") + } + return value, nil +} + +func CleanupExpired(store *storage.Store, now time.Time) (int, error) { + if store == nil { + return 0, errors.New("repository task log cleanup requires storage") + } + root, err := store.Resolve("task-logs") + if err != nil { + return 0, err + } + entries, err := os.ReadDir(root) + if err != nil { + return 0, err + } + removed := 0 + for _, entry := range entries { + if !entry.IsDir() || uuid.Validate(entry.Name()) != nil { + continue + } + metadata, readErr := ReadMetadata(store, entry.Name()) + if readErr != nil || metadata.ExpiresAt.After(now.UTC()) { + continue + } + path := filepath.Join(root, entry.Name()) + if err = os.RemoveAll(path); err != nil { + return removed, err + } + removed++ + } + return removed, nil +} + +func Redact(value string) string { + // Repository syncs can emit gigabytes of ordinary output. Avoid running every + // regular expression over every line while preserving the same redaction + // rules whenever a sensitive marker is present. + lower := strings.ToLower(value) + if strings.Contains(value, "-----BEGIN PGP PRIVATE KEY BLOCK-----") { + value = privateKeyPattern.ReplaceAllString(value, "[REDACTED_PRIVATE_KEY]") + lower = strings.ToLower(value) + } + if strings.Contains(lower, "-----begin private key-----") || + strings.Contains(lower, "-----begin rsa private key-----") || + strings.Contains(lower, "-----begin ec private key-----") || + strings.Contains(lower, "-----begin openssh private key-----") { + value = pemPrivateKeyPattern.ReplaceAllString(value, "[REDACTED_PRIVATE_KEY]") + value = escapedPEMKeyPattern.ReplaceAllString(value, "[REDACTED_PRIVATE_KEY]") + lower = strings.ToLower(value) + } + if strings.Contains(value, "://") && strings.Contains(value, "@") { + value = credentialURLPattern.ReplaceAllString(value, `${1}[REDACTED]@`) + lower = strings.ToLower(value) + } + if strings.Contains(lower, "bearer ") || strings.Contains(lower, "basic ") { + value = bearerPattern.ReplaceAllString(value, `${1} [REDACTED]`) + lower = strings.ToLower(value) + } + if containsSecretMarker(lower) { + value = secretJSONPattern.ReplaceAllString(value, `${1}[REDACTED]${3}`) + value = secretPattern.ReplaceAllString(value, `${1}${2}[REDACTED]`) + } + return value +} + +func containsSecretMarker(lower string) bool { + for _, marker := range [...]string{ + "authorization", "proxy-authorization", "password", "passwd", + "token", "secret", "credential", "private_key", "private-key", "api_key", "api-key", + } { + if strings.Contains(lower, marker) { + return true + } + } + return false +} + +func (w *Writer) addSummary(level, line string) { + line = bounded(line, 4096) + if len(w.first) < 8 { + w.first = append(w.first, line) + } + if level == "ERROR" || level == "WARN" { + if len(w.errors) < 32 { + w.errors = append(w.errors, line) + } + } + w.last = append(w.last, line) + if len(w.last) > 8 { + w.last = w.last[len(w.last)-8:] + } +} + +func (w *Writer) summary() string { + parts := make([]string, 0, len(w.first)+len(w.errors)+len(w.last)+3) + parts = append(parts, "[first]") + parts = append(parts, w.first...) + if len(w.errors) > 0 { + parts = append(parts, "[warnings-and-errors]") + parts = append(parts, w.errors...) + } + parts = append(parts, "[last]") + parts = append(parts, w.last...) + return bounded(strings.Join(parts, "\n"), MaximumSummaryBytes) +} + +func bounded(value string, maximum int) string { + if len(value) <= maximum { + return value + } + for maximum > 0 && !utf8.ValidString(value[:maximum]) { + maximum-- + } + return value[:maximum] +} diff --git a/manager/backend/services/footstone/infrastructure/repository/tasklog/tasklog_test.go b/manager/backend/services/footstone/infrastructure/repository/tasklog/tasklog_test.go new file mode 100644 index 0000000000000000000000000000000000000000..3462962ee632dfbd6bccf5bfd9210f5211fed270 --- /dev/null +++ b/manager/backend/services/footstone/infrastructure/repository/tasklog/tasklog_test.go @@ -0,0 +1,192 @@ +package tasklog + +import ( + "context" + "io" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/klauspost/compress/zstd" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" +) + +const testTaskID = "11111111-1111-4111-8111-111111111111" + +func TestTaskLogStreamsCompressesAndRedacts(t *testing.T) { + store, err := storage.New(config.RepositoryConfig{Root: t.TempDir(), MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + writer, err := New(store, testTaskID) + if err != nil { + t.Fatal(err) + } + now := time.Date(2026, 8, 31, 12, 0, 0, 0, time.UTC) + writer.now = func() time.Time { return now } + ctx := WithWriter(context.Background(), writer) + if err = Event(ctx, "INFO", "download", "GET https://user:secret@example.test/pkg.rpm Authorization: Bearer abc123", + map[string]string{"api_key": "api_key=top-secret"}); err != nil { + t.Fatal(err) + } + if err = Event(ctx, "ERROR", "verify", "password=hunter2 checksum failed", nil); err != nil { + t.Fatal(err) + } + metadata, err := writer.Close("FAILED", "CHECKSUM_FAILED", "password=hunter2 checksum failed") + if err != nil { + t.Fatal(err) + } + if metadata.LineCount != 2 || metadata.ErrorFingerprint == "" || + metadata.ExpiresAt.Sub(now) != FailureRetention || len(metadata.Summary) > MaximumSummaryBytes { + t.Fatalf("metadata = %#v", metadata) + } + if strings.Contains(metadata.Summary, "secret") || strings.Contains(metadata.Summary, "hunter2") || + strings.Contains(metadata.Summary, "abc123") { + t.Fatalf("summary leaked a secret: %s", metadata.Summary) + } + path, _ := store.Resolve(metadata.RelativePath) + file, err := os.Open(path) + if err != nil { + t.Fatal(err) + } + decompressed, err := zstd.NewReader(file) + if err != nil { + t.Fatal(err) + } + content := new(strings.Builder) + if _, err = io.Copy(content, decompressed); err != nil { + t.Fatal(err) + } + decompressed.Close() + file.Close() + if strings.Contains(content.String(), "secret") || strings.Contains(content.String(), "hunter2") || + strings.Contains(content.String(), "abc123") || !strings.Contains(content.String(), "[REDACTED]") { + t.Fatalf("raw log redaction failed: %s", content.String()) + } + read, err := ReadMetadata(store, testTaskID) + if err != nil || read.SHA256 != metadata.SHA256 { + t.Fatalf("ReadMetadata() = %#v/%v", read, err) + } +} + +func TestRedactKeepsCaseInsensitiveCredentialCoverage(t *testing.T) { + privateKeyBlock := "-----BEGIN " + "PRIVATE KEY-----\nprivate-pem-material\n-----END " + "PRIVATE KEY-----" + input := "PaSsWoRd=hunter2 AUTHORIZATION: BeArEr mixed-token " + + "https://user:secret@example.test/pkg.rpm ApI_KeY=private " + + "node_private_key=private-key-material tms_credential=credential-value " + + `{"bootstrap_token":"json-token","password":"json-password"}` + " " + + privateKeyBlock + redacted := Redact(input) + for _, secret := range []string{"hunter2", "mixed-token", "user:secret", "private-key-material", "credential-value", + "json-token", "json-password", "private-pem-material"} { + if strings.Contains(redacted, secret) { + t.Fatalf("Redact() leaked %q: %s", secret, redacted) + } + } + if !strings.Contains(redacted, "[REDACTED]") { + t.Fatalf("Redact() did not mark credentials: %s", redacted) + } +} + +func TestTaskLogSummaryBoundAndCleanup(t *testing.T) { + store, err := storage.New(config.RepositoryConfig{Root: t.TempDir(), MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + writer, err := New(store, testTaskID) + if err != nil { + t.Fatal(err) + } + now := time.Date(2026, 8, 31, 12, 0, 0, 0, time.UTC) + writer.now = func() time.Time { return now } + for index := 0; index < 1000; index++ { + if err = writer.Event("ERROR", "download", strings.Repeat("x", 4096), nil); err != nil { + t.Fatal(err) + } + } + metadata, err := writer.Close("SUCCESS", "", "") + if err != nil { + t.Fatal(err) + } + if len(metadata.Summary) > MaximumSummaryBytes || metadata.UncompressedBytes < 4<<20 || + metadata.ExpiresAt.Sub(now) != SuccessRetention { + t.Fatalf("bounded metadata = %#v", metadata) + } + if count, err := CleanupExpired(store, now.Add(SuccessRetention-time.Second)); err != nil || count != 0 { + t.Fatalf("early cleanup = %d/%v", count, err) + } + if count, err := CleanupExpired(store, now.Add(SuccessRetention)); err != nil || count != 1 { + t.Fatalf("expired cleanup = %d/%v", count, err) + } +} + +func TestTaskLogPreservesPreviousLeaseAttempt(t *testing.T) { + store, err := storage.New(config.RepositoryConfig{Root: t.TempDir(), MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + first, err := New(store, testTaskID) + if err != nil { + t.Fatal(err) + } + if err = first.Event("WARN", "lease", "control plane heartbeat failed", nil); err != nil { + t.Fatal(err) + } + firstMetadata, err := first.Close("FAILED", "TASK_HEARTBEAT_FAILED", "control plane unavailable") + if err != nil { + t.Fatal(err) + } + + second, err := New(store, testTaskID) + if err != nil { + t.Fatalf("recovered task log writer = %v", err) + } + if err = second.Event("INFO", "lease", "task reclaimed with a new fencing token", nil); err != nil { + t.Fatal(err) + } + secondMetadata, err := second.Close("SUCCESS", "", "") + if err != nil { + t.Fatal(err) + } + current, err := ReadMetadata(store, testTaskID) + if err != nil || current.SHA256 != secondMetadata.SHA256 || current.SHA256 == firstMetadata.SHA256 { + t.Fatalf("current recovered metadata = %#v/%v", current, err) + } + recoveredDirectory, err := store.Resolve(filepath.ToSlash(filepath.Join("task-logs", testTaskID, "recovered"))) + if err != nil { + t.Fatal(err) + } + entries, err := os.ReadDir(recoveredDirectory) + if err != nil || len(entries) != 2 { + t.Fatalf("recovered task log entries = %d/%v", len(entries), err) + } + var archivedContent string + for _, entry := range entries { + if !strings.HasSuffix(entry.Name(), ".jsonl.zst") { + continue + } + file, openErr := os.Open(filepath.Join(recoveredDirectory, entry.Name())) + if openErr != nil { + t.Fatal(openErr) + } + decoder, decodeErr := zstd.NewReader(file) + if decodeErr != nil { + file.Close() + t.Fatal(decodeErr) + } + content, readErr := io.ReadAll(decoder) + decoder.Close() + file.Close() + if readErr != nil { + t.Fatal(readErr) + } + archivedContent = string(content) + } + if !strings.Contains(archivedContent, "control plane heartbeat failed") { + t.Fatalf("archived task log content = %q", archivedContent) + } +} diff --git a/manager/backend/services/footstone/infrastructure/repository/taskresult/failure.go b/manager/backend/services/footstone/infrastructure/repository/taskresult/failure.go new file mode 100644 index 0000000000000000000000000000000000000000..504b222661ad23101ef165a54ffdc79a0a69d65c --- /dev/null +++ b/manager/backend/services/footstone/infrastructure/repository/taskresult/failure.go @@ -0,0 +1,178 @@ +package taskresult + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "sort" + "strings" + "unicode" + "unicode/utf8" + + "github.com/google/uuid" + "github.com/klauspost/compress/zstd" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +const failureRecordType = "failure" + +type FailureRecord struct { + RecordType string `json:"record_type"` + SchemaVersion int `json:"schema_version"` + TaskID string `json:"task_id"` + RepositoryID string `json:"repository_id"` + StorageNodeID string `json:"storage_node_id"` + UploadID string `json:"upload_id"` + CompletionStatus string `json:"completion_status"` + QuarantineCount int `json:"quarantine_count"` + ErrorCode string `json:"error_code"` + ErrorMessage string `json:"error_message"` +} + +type FailureDocument struct { + Failure FailureRecord + Quarantine []QuarantineRecord +} + +type FailureExpectation struct { + TaskID string + RepositoryID string + StorageNodeID string + UploadID string + ErrorCode string + QuarantineCount int64 + MaxQuarantine int + MaxExpandedBytes int64 +} + +func WriteFailureZstd(destination io.Writer, task *po.RepositoryTask, upload *po.RepositoryUpload, + quarantine []po.RepositoryQuarantineItem, errorCode, errorMessage string, +) error { + if destination == nil || task == nil || upload == nil { + return errors.New("repository failure writer requires destination, task, and upload") + } + compressed, err := zstd.NewWriter(destination, zstd.WithEncoderLevel(zstd.SpeedDefault)) + if err != nil { + return err + } + encoder := json.NewEncoder(compressed) + err = encoder.Encode(FailureRecord{RecordType: failureRecordType, SchemaVersion: SchemaVersion, + TaskID: task.ID, RepositoryID: task.RepositoryID, StorageNodeID: task.StorageNodeID, + UploadID: upload.ID, CompletionStatus: request.RepositoryTaskStatusFailed, + QuarantineCount: len(quarantine), ErrorCode: errorCode, ErrorMessage: errorMessage}) + if err == nil { + sorted := append([]po.RepositoryQuarantineItem(nil), quarantine...) + sort.Slice(sorted, func(i, j int) bool { return sorted[i].RelativePath < sorted[j].RelativePath }) + for _, item := range sorted { + err = encoder.Encode(QuarantineRecord{RecordType: quarantineRecordType, + RepositoryID: item.RepositoryID, TaskID: item.TaskID, UploadID: item.UploadID, + OriginalName: item.OriginalName, RelativePath: item.RelativePath, + SizeBytes: item.SizeBytes, SHA256: item.SHA256, ReasonCode: item.ReasonCode, + ReasonMessage: item.ReasonMessage, DetectedNEVRA: item.DetectedNEVRA}) + if err != nil { + break + } + } + } + if closeErr := compressed.Close(); err == nil { + err = closeErr + } + return err +} + +func ParseFailureZstd(source io.Reader, expected FailureExpectation) (*FailureDocument, error) { + if source == nil { + return nil, errors.New("repository failure result source is required") + } + if expected.MaxQuarantine <= 0 { + expected.MaxQuarantine = DefaultMaxQuarantineRecords + } + if expected.MaxExpandedBytes <= 0 { + expected.MaxExpandedBytes = DefaultMaxExpandedBytes + } + reader, err := zstd.NewReader(source, zstd.WithDecoderMaxMemory(uint64(expected.MaxExpandedBytes))) + if err != nil { + return nil, fmt.Errorf("open repository failure result compression: %w", err) + } + defer reader.Close() + limited := &io.LimitedReader{R: reader, N: expected.MaxExpandedBytes + 1} + decoder := json.NewDecoder(limited) + var raw json.RawMessage + if err = decoder.Decode(&raw); err != nil { + return nil, fmt.Errorf("decode repository failure result: %w", err) + } + if len(raw) > maximumRecordBytes { + return nil, errors.New("repository failure result record exceeds the size limit") + } + document := new(FailureDocument) + if err = decodeStrict(raw, &document.Failure); err != nil { + return nil, fmt.Errorf("decode repository failure result: %w", err) + } + if err = validateFailure(document.Failure, expected); err != nil { + return nil, err + } + seen := make(map[string]struct{}, document.Failure.QuarantineCount) + for { + raw = nil + err = decoder.Decode(&raw) + if errors.Is(err, io.EOF) { + break + } + if err != nil { + return nil, fmt.Errorf("decode repository failure quarantine: %w", err) + } + if len(raw) > maximumRecordBytes || len(document.Quarantine) >= expected.MaxQuarantine { + return nil, errors.New("repository failure quarantine exceeds configured limits") + } + var item QuarantineRecord + if err = decodeStrict(raw, &item); err != nil { + return nil, fmt.Errorf("decode repository failure quarantine: %w", err) + } + revision := RevisionRecord{TaskID: document.Failure.TaskID, RepositoryID: document.Failure.RepositoryID} + if err = validateQuarantine(item, revision, Expectation{UploadID: expected.UploadID}); err != nil { + return nil, err + } + if _, exists := seen[item.RelativePath]; exists { + return nil, fmt.Errorf("repository failure contains duplicate quarantine path %q", item.RelativePath) + } + seen[item.RelativePath] = struct{}{} + document.Quarantine = append(document.Quarantine, item) + } + if limited.N <= 0 { + return nil, fmt.Errorf("repository failure result exceeds expanded size limit %d", expected.MaxExpandedBytes) + } + if len(document.Quarantine) != document.Failure.QuarantineCount { + return nil, errors.New("repository failure quarantine count mismatch") + } + return document, nil +} + +func validateFailure(value FailureRecord, expected FailureExpectation) error { + if value.RecordType != failureRecordType || value.SchemaVersion != SchemaVersion || + value.CompletionStatus != request.RepositoryTaskStatusFailed || value.QuarantineCount <= 0 || + value.QuarantineCount > expected.MaxQuarantine || !errorCodePattern.MatchString(value.ErrorCode) || + value.ErrorMessage == "" || utf8.RuneCountInString(value.ErrorMessage) > 2048 || + strings.IndexFunc(value.ErrorMessage, unicode.IsControl) >= 0 { + return errors.New("repository failure result metadata is invalid") + } + if uuid.Validate(value.TaskID) != nil || uuid.Validate(value.RepositoryID) != nil || + uuid.Validate(value.StorageNodeID) != nil || uuid.Validate(value.UploadID) != nil { + return errors.New("repository failure result identity is invalid") + } + for _, identity := range []struct{ actual, wanted string }{ + {value.TaskID, expected.TaskID}, {value.RepositoryID, expected.RepositoryID}, + {value.StorageNodeID, expected.StorageNodeID}, {value.UploadID, expected.UploadID}, + {value.ErrorCode, expected.ErrorCode}, + } { + if identity.wanted != "" && identity.actual != identity.wanted { + return errors.New("repository failure result identity or error code mismatch") + } + } + if expected.QuarantineCount >= 0 && int64(value.QuarantineCount) != expected.QuarantineCount { + return errors.New("repository failure result quarantine count does not match report") + } + return nil +} diff --git a/manager/backend/services/footstone/infrastructure/repository/taskresult/failure_test.go b/manager/backend/services/footstone/infrastructure/repository/taskresult/failure_test.go new file mode 100644 index 0000000000000000000000000000000000000000..26bbb40a0ce1de378846cb52e2c29819dc1af1ec --- /dev/null +++ b/manager/backend/services/footstone/infrastructure/repository/taskresult/failure_test.go @@ -0,0 +1,36 @@ +package taskresult + +import ( + "bytes" + "strings" + "testing" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +func TestWriteAndParseFailureResult(t *testing.T) { + uploadID := "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee" + quarantine := po.RepositoryQuarantineItem{RepositoryID: testRepositoryID, TaskID: testTaskID, + UploadID: uploadID, OriginalName: "damaged.rpm", + RelativePath: "quarantine/" + testRepositoryID + "/" + testTaskID + "/damaged.rpm", + SizeBytes: 5, SHA256: strings.Repeat("5", 64), ReasonCode: request.RepositoryQuarantineDamagedRPM, + ReasonMessage: "rpm integrity verification failed"} + var output bytes.Buffer + err := WriteFailureZstd(&output, &po.RepositoryTask{ID: testTaskID, RepositoryID: testRepositoryID, + StorageNodeID: testNodeID}, &po.RepositoryUpload{ID: uploadID}, + []po.RepositoryQuarantineItem{quarantine}, "ALL_PACKAGES_REJECTED", "all packages were rejected") + if err != nil { + t.Fatal(err) + } + document, err := ParseFailureZstd(bytes.NewReader(output.Bytes()), FailureExpectation{TaskID: testTaskID, + RepositoryID: testRepositoryID, StorageNodeID: testNodeID, UploadID: uploadID, + ErrorCode: "ALL_PACKAGES_REJECTED", QuarantineCount: 1}) + if err != nil { + t.Fatal(err) + } + if document.Failure.QuarantineCount != 1 || len(document.Quarantine) != 1 || + document.Quarantine[0].ReasonCode != request.RepositoryQuarantineDamagedRPM { + t.Fatalf("failure result = %#v", document) + } +} diff --git a/manager/backend/services/footstone/infrastructure/repository/taskresult/result.go b/manager/backend/services/footstone/infrastructure/repository/taskresult/result.go new file mode 100644 index 0000000000000000000000000000000000000000..16766858fa0b957928ce6832c06090995c2b1c82 --- /dev/null +++ b/manager/backend/services/footstone/infrastructure/repository/taskresult/result.go @@ -0,0 +1,513 @@ +package taskresult + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "net/url" + "path" + "regexp" + "sort" + "strings" + "unicode" + "unicode/utf8" + + "github.com/google/uuid" + "github.com/klauspost/compress/zstd" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +const ( + SchemaVersion = 1 + DefaultMaxExpandedBytes = 256 << 20 + DefaultMaxPackageRecords = 1000000 + DefaultMaxQuarantineRecords = 10000 + maximumRecordBytes = 1 << 20 + revisionRecordType = "revision" + packageRecordType = "package" + quarantineRecordType = "quarantine" +) + +var sha256Pattern = regexp.MustCompile(`^[0-9a-f]{64}$`) +var errorCodePattern = regexp.MustCompile(`^[A-Z0-9_]{1,64}$`) +var metadataSigningKeyRefPattern = regexp.MustCompile(`^[A-Za-z0-9._+-]{1,128}$`) + +// RevisionRecord is the first JSONL record in a repository-node result object. +type RevisionRecord struct { + RecordType string `json:"record_type"` + SchemaVersion int `json:"schema_version"` + TaskID string `json:"task_id"` + RepositoryID string `json:"repository_id"` + StorageNodeID string `json:"storage_node_id"` + RevisionID string `json:"revision_id"` + ParentRevisionID string `json:"parent_revision_id,omitempty"` + Kind string `json:"kind"` + PackageCount int `json:"package_count"` + TotalBytes int64 `json:"total_bytes"` + ManifestSHA256 string `json:"manifest_sha256"` + RepomdSHA256 string `json:"repomd_sha256"` + MetadataSigned bool `json:"metadata_signed,omitempty"` + MetadataSigningKeyRef string `json:"metadata_signing_key_ref,omitempty"` + MetadataSigningFingerprint string `json:"metadata_signing_fingerprint,omitempty"` + RepomdSignaturePath string `json:"repomd_signature_path,omitempty"` + MetadataPublicKeyURL string `json:"metadata_public_key_url,omitempty"` + SnapshotManifestSHA256 string `json:"snapshot_manifest_sha256"` + RelativePath string `json:"relative_path"` + CompletionStatus string `json:"completion_status"` + AcceptedCount int `json:"accepted_count"` + RejectedCount int `json:"rejected_count"` + ErrorCode string `json:"error_code,omitempty"` + ErrorMessage string `json:"error_message,omitempty"` +} + +// PackageRecord describes one immutable RPM stored in the published revision. +type PackageRecord struct { + RecordType string `json:"record_type"` + RepositoryID string `json:"repository_id"` + RevisionID string `json:"revision_id"` + Name string `json:"name"` + Epoch string `json:"epoch"` + Version string `json:"version"` + Release string `json:"release"` + Architecture string `json:"architecture"` + NEVRA string `json:"nevra"` + FileName string `json:"file_name"` + RelativePath string `json:"relative_path"` + SizeBytes int64 `json:"size_bytes"` + SHA256 string `json:"sha256"` + Source string `json:"source"` + SourceUploadID string `json:"source_upload_id,omitempty"` + SignatureStatus string `json:"signature_status,omitempty"` + SignerKeyID string `json:"signer_key_id,omitempty"` + SignatureMessage string `json:"signature_message,omitempty"` +} + +// QuarantineRecord describes a rejected upload object outside the active revision. +type QuarantineRecord struct { + RecordType string `json:"record_type"` + RepositoryID string `json:"repository_id"` + TaskID string `json:"task_id"` + UploadID string `json:"upload_id"` + OriginalName string `json:"original_name"` + RelativePath string `json:"relative_path"` + SizeBytes int64 `json:"size_bytes"` + SHA256 string `json:"sha256"` + ReasonCode string `json:"reason_code"` + ReasonMessage string `json:"reason_message"` + DetectedNEVRA string `json:"detected_nevra,omitempty"` +} + +type Document struct { + Revision RevisionRecord + Packages []PackageRecord + Quarantine []QuarantineRecord +} + +type WriteOptions struct { + CompletionStatus string + AcceptedCount int + RejectedCount int + ErrorCode string + ErrorMessage string + Quarantine []po.RepositoryQuarantineItem +} + +type Expectation struct { + TaskID string + RepositoryID string + StorageNodeID string + RevisionID string + PublishKey string + ManifestSHA256 string + Kind string + Architecture string + PackageCount int64 + TotalBytes int64 + MaxPackages int + MaxQuarantine int + MaxExpandedBytes int64 + CompletionStatus string + UploadID string +} + +func WriteZstd(destination io.Writer, task *po.RepositoryTask, revision *po.RepositoryRevision, + packages []po.RepositoryPackage, +) error { + return WriteZstdWithOptions(destination, task, revision, packages, WriteOptions{ + CompletionStatus: request.RepositoryTaskStatusSuccess, AcceptedCount: len(packages), + }) +} + +func WriteZstdWithOptions(destination io.Writer, task *po.RepositoryTask, revision *po.RepositoryRevision, + packages []po.RepositoryPackage, options WriteOptions, +) error { + if destination == nil || task == nil || revision == nil { + return errors.New("repository result writer requires destination, task, and revision") + } + compressed, err := zstd.NewWriter(destination, zstd.WithEncoderLevel(zstd.SpeedDefault)) + if err != nil { + return err + } + encoder := json.NewEncoder(compressed) + header := RevisionRecord{RecordType: revisionRecordType, SchemaVersion: SchemaVersion, + TaskID: task.ID, RepositoryID: revision.RepositoryID, StorageNodeID: revision.StorageNodeID, + RevisionID: revision.ID, ParentRevisionID: revision.ParentRevisionID, Kind: revision.Kind, + PackageCount: revision.PackageCount, TotalBytes: revision.TotalBytes, + ManifestSHA256: revision.ManifestSHA256, RepomdSHA256: revision.RepomdSHA256, + MetadataSigned: revision.MetadataSigned, MetadataSigningKeyRef: revision.MetadataSigningKeyRef, + MetadataSigningFingerprint: revision.MetadataSigningFingerprint, + RepomdSignaturePath: revision.RepomdSignaturePath, + MetadataPublicKeyURL: revision.MetadataPublicKeyURL, + SnapshotManifestSHA256: revision.SnapshotManifestSHA256, RelativePath: revision.RelativePath, + CompletionStatus: options.CompletionStatus, AcceptedCount: options.AcceptedCount, + RejectedCount: options.RejectedCount, ErrorCode: options.ErrorCode, ErrorMessage: options.ErrorMessage} + if err = encoder.Encode(header); err == nil { + sorted := append([]po.RepositoryPackage(nil), packages...) + sort.Slice(sorted, func(i, j int) bool { return sorted[i].NEVRA < sorted[j].NEVRA }) + for _, item := range sorted { + err = encoder.Encode(PackageRecord{RecordType: packageRecordType, RepositoryID: item.RepositoryID, + RevisionID: item.RevisionID, Name: item.Name, Epoch: item.Epoch, Version: item.Version, + Release: item.Release, Architecture: item.Architecture, NEVRA: item.NEVRA, + FileName: item.FileName, RelativePath: item.RelativePath, SizeBytes: item.SizeBytes, + SHA256: item.SHA256, Source: item.Source, SourceUploadID: item.SourceUploadID, + SignatureStatus: item.SignatureStatus, SignerKeyID: item.SignerKeyID, + SignatureMessage: item.SignatureMessage}) + if err != nil { + break + } + } + if err == nil { + sortedQuarantine := append([]po.RepositoryQuarantineItem(nil), options.Quarantine...) + sort.Slice(sortedQuarantine, func(i, j int) bool { + return sortedQuarantine[i].RelativePath < sortedQuarantine[j].RelativePath + }) + for _, item := range sortedQuarantine { + err = encoder.Encode(QuarantineRecord{RecordType: quarantineRecordType, + RepositoryID: item.RepositoryID, TaskID: item.TaskID, UploadID: item.UploadID, + OriginalName: item.OriginalName, RelativePath: item.RelativePath, + SizeBytes: item.SizeBytes, SHA256: item.SHA256, ReasonCode: item.ReasonCode, + ReasonMessage: item.ReasonMessage, DetectedNEVRA: item.DetectedNEVRA}) + if err != nil { + break + } + } + } + } + if closeErr := compressed.Close(); err == nil { + err = closeErr + } + return err +} + +func ParseZstd(source io.Reader, expected Expectation) (*Document, error) { + if source == nil { + return nil, errors.New("repository result source is required") + } + if expected.MaxPackages <= 0 { + expected.MaxPackages = DefaultMaxPackageRecords + } + if expected.MaxQuarantine <= 0 { + expected.MaxQuarantine = DefaultMaxQuarantineRecords + } + if expected.MaxExpandedBytes <= 0 { + expected.MaxExpandedBytes = DefaultMaxExpandedBytes + } + reader, err := zstd.NewReader(source, zstd.WithDecoderMaxMemory(uint64(expected.MaxExpandedBytes))) + if err != nil { + return nil, fmt.Errorf("open repository result compression: %w", err) + } + defer reader.Close() + limited := &io.LimitedReader{R: reader, N: expected.MaxExpandedBytes + 1} + decoder := json.NewDecoder(limited) + var raw json.RawMessage + if err = decoder.Decode(&raw); err != nil { + return nil, fmt.Errorf("decode repository revision result: %w", err) + } + if len(raw) > maximumRecordBytes { + return nil, errors.New("repository revision result record exceeds the size limit") + } + document := new(Document) + if err = decodeStrict(raw, &document.Revision); err != nil { + return nil, fmt.Errorf("decode repository revision result: %w", err) + } + if err = validateRevision(document.Revision, expected); err != nil { + return nil, err + } + seenNEVRA := make(map[string]struct{}, document.Revision.PackageCount) + seenPath := make(map[string]struct{}, document.Revision.PackageCount) + seenQuarantinePath := make(map[string]struct{}, document.Revision.RejectedCount) + for { + raw = nil + err = decoder.Decode(&raw) + if errors.Is(err, io.EOF) { + break + } + if err != nil { + return nil, fmt.Errorf("decode repository result record: %w", err) + } + if len(raw) > maximumRecordBytes { + return nil, errors.New("repository result record exceeds the size limit") + } + var envelope struct { + RecordType string `json:"record_type"` + } + if err = json.Unmarshal(raw, &envelope); err != nil { + return nil, fmt.Errorf("decode repository result record type: %w", err) + } + switch envelope.RecordType { + case packageRecordType: + if len(document.Packages) >= expected.MaxPackages { + return nil, fmt.Errorf("repository result exceeds package record limit %d", expected.MaxPackages) + } + var item PackageRecord + if err = decodeStrict(raw, &item); err != nil { + return nil, fmt.Errorf("decode repository package result: %w", err) + } + if err = validatePackage(item, document.Revision, expected); err != nil { + return nil, err + } + if _, exists := seenNEVRA[item.NEVRA]; exists { + return nil, fmt.Errorf("repository result contains duplicate NEVRA %q", item.NEVRA) + } + if _, exists := seenPath[item.RelativePath]; exists { + return nil, fmt.Errorf("repository result contains duplicate package path %q", item.RelativePath) + } + seenNEVRA[item.NEVRA], seenPath[item.RelativePath] = struct{}{}, struct{}{} + document.Packages = append(document.Packages, item) + case quarantineRecordType: + if len(document.Quarantine) >= expected.MaxQuarantine { + return nil, fmt.Errorf("repository result exceeds quarantine record limit %d", expected.MaxQuarantine) + } + var item QuarantineRecord + if err = decodeStrict(raw, &item); err != nil { + return nil, fmt.Errorf("decode repository quarantine result: %w", err) + } + if err = validateQuarantine(item, document.Revision, expected); err != nil { + return nil, err + } + if _, exists := seenQuarantinePath[item.RelativePath]; exists { + return nil, fmt.Errorf("repository result contains duplicate quarantine path %q", item.RelativePath) + } + seenQuarantinePath[item.RelativePath] = struct{}{} + document.Quarantine = append(document.Quarantine, item) + default: + return nil, fmt.Errorf("unsupported repository result record type %q", envelope.RecordType) + } + } + if limited.N <= 0 { + return nil, fmt.Errorf("repository result exceeds expanded size limit %d", expected.MaxExpandedBytes) + } + if err = validateAggregate(document, expected); err != nil { + return nil, err + } + return document, nil +} + +func decodeStrict(raw []byte, destination interface{}) error { + decoder := json.NewDecoder(strings.NewReader(string(raw))) + decoder.DisallowUnknownFields() + if err := decoder.Decode(destination); err != nil { + return err + } + if decoder.Decode(new(interface{})) != io.EOF { + return errors.New("record contains trailing JSON data") + } + return nil +} + +func validateRevision(value RevisionRecord, expected Expectation) error { + if value.RecordType != revisionRecordType || value.SchemaVersion != SchemaVersion { + return fmt.Errorf("unsupported repository result record %q schema %d", value.RecordType, value.SchemaVersion) + } + if uuid.Validate(value.TaskID) != nil || uuid.Validate(value.RepositoryID) != nil || + uuid.Validate(value.StorageNodeID) != nil || uuid.Validate(value.RevisionID) != nil { + return errors.New("repository result contains an invalid identity") + } + if value.ParentRevisionID != "" && uuid.Validate(value.ParentRevisionID) != nil { + return errors.New("repository result contains an invalid parent revision identity") + } + identities := []struct{ name, actual, wanted string }{ + {"task", value.TaskID, expected.TaskID}, {"repository", value.RepositoryID, expected.RepositoryID}, + {"storage node", value.StorageNodeID, expected.StorageNodeID}, {"revision", value.RevisionID, expected.RevisionID}, + } + for _, identity := range identities { + if identity.wanted != "" && identity.actual != identity.wanted { + return fmt.Errorf("repository result %s identity mismatch", identity.name) + } + } + if expected.Kind != "" && value.Kind != expected.Kind { + return fmt.Errorf("repository result revision kind mismatch") + } + if expected.CompletionStatus != "" && value.CompletionStatus != expected.CompletionStatus { + return fmt.Errorf("repository result completion status mismatch") + } + if value.PackageCount < 0 || value.TotalBytes < 0 || value.PackageCount > expected.MaxPackages { + return errors.New("repository result revision aggregate is invalid") + } + if value.AcceptedCount < 0 || value.RejectedCount < 0 || value.AcceptedCount > value.PackageCount { + return errors.New("repository result ingestion aggregate is invalid") + } + if utf8.RuneCountInString(value.ErrorMessage) > 2048 || + strings.IndexFunc(value.ErrorMessage, unicode.IsControl) >= 0 { + return errors.New("repository result error message is invalid") + } + switch value.CompletionStatus { + case request.RepositoryTaskStatusSuccess: + if value.RejectedCount != 0 || value.ErrorCode != "" || value.ErrorMessage != "" { + return errors.New("successful repository result contains rejection details") + } + case request.RepositoryTaskStatusPartialSuccess: + if value.Kind != request.RepositoryRevisionKindUpload || value.AcceptedCount == 0 || + value.RejectedCount == 0 || !errorCodePattern.MatchString(value.ErrorCode) { + return errors.New("partial repository result is not a valid upload outcome") + } + default: + return fmt.Errorf("repository result completion status %q is unsupported", value.CompletionStatus) + } + if value.Kind == request.RepositoryRevisionKindSync && value.AcceptedCount != value.PackageCount { + return errors.New("sync result accepted count does not match package count") + } + for name, digest := range map[string]string{"manifest": value.ManifestSHA256, + "repomd": value.RepomdSHA256, "snapshot manifest": value.SnapshotManifestSHA256} { + if !sha256Pattern.MatchString(digest) { + return fmt.Errorf("repository result %s digest is invalid", name) + } + } + if expected.ManifestSHA256 != "" && value.SnapshotManifestSHA256 != expected.ManifestSHA256 { + return errors.New("repository result snapshot manifest digest mismatch") + } + expectedPath := path.Join("public", expected.PublishKey, "revisions", value.RevisionID) + if expected.PublishKey == "" || !safeRelativePath(value.RelativePath) || value.RelativePath != expectedPath { + return errors.New("repository result revision path is invalid") + } + if value.MetadataSigned { + publicURL, parseErr := url.Parse(value.MetadataPublicKeyURL) + if !metadataSigningKeyRefPattern.MatchString(value.MetadataSigningKeyRef) || + len(value.MetadataSigningFingerprint) < 40 || len(value.MetadataSigningFingerprint) > 64 || + value.RepomdSignaturePath != "repodata/repomd.xml.asc" || parseErr != nil || + (publicURL.Scheme != "http" && publicURL.Scheme != "https") || publicURL.Host == "" || + publicURL.User != nil || publicURL.RawQuery != "" || publicURL.Fragment != "" { + return errors.New("repository result metadata signing detail is invalid") + } + } else if value.MetadataSigningKeyRef != "" || value.MetadataSigningFingerprint != "" || + value.RepomdSignaturePath != "" || value.MetadataPublicKeyURL != "" { + return errors.New("unsigned repository result contains metadata signing detail") + } + return nil +} + +func validateQuarantine(value QuarantineRecord, revision RevisionRecord, expected Expectation) error { + if value.RecordType != quarantineRecordType || value.RepositoryID != revision.RepositoryID || + value.TaskID != revision.TaskID || uuid.Validate(value.UploadID) != nil { + return errors.New("repository quarantine result identity is invalid") + } + if expected.UploadID != "" && value.UploadID != expected.UploadID { + return errors.New("repository quarantine result upload identity mismatch") + } + if !safeName(value.OriginalName) || value.SizeBytes <= 0 || !sha256Pattern.MatchString(value.SHA256) || + !errorCodePattern.MatchString(value.ReasonCode) { + return errors.New("repository quarantine result metadata is invalid") + } + wantedPrefix := path.Join("quarantine", revision.RepositoryID, revision.TaskID) + "/" + if !safeRelativePath(value.RelativePath) || !strings.HasPrefix(value.RelativePath, wantedPrefix) { + return errors.New("repository quarantine result path is invalid") + } + if value.ReasonMessage == "" || utf8.RuneCountInString(value.ReasonMessage) > 2048 || + strings.IndexFunc(value.ReasonMessage, unicode.IsControl) >= 0 || + utf8.RuneCountInString(value.DetectedNEVRA) > 1024 || + strings.IndexFunc(value.DetectedNEVRA, unicode.IsControl) >= 0 { + return errors.New("repository quarantine result detail is invalid") + } + return nil +} + +func validatePackage(value PackageRecord, revision RevisionRecord, expected Expectation) error { + if value.RecordType != packageRecordType || value.RepositoryID != revision.RepositoryID || + value.RevisionID != revision.RevisionID { + return errors.New("repository package result identity is invalid") + } + if value.Name == "" || value.Epoch == "" || value.Version == "" || value.Release == "" || + value.Architecture == "" || value.NEVRA == "" || value.FileName == "" || value.SizeBytes <= 0 { + return errors.New("repository package result metadata is incomplete") + } + if expected.Architecture != "" && value.Architecture != expected.Architecture && value.Architecture != "noarch" { + return fmt.Errorf("repository package %q architecture is not allowed", value.NEVRA) + } + wantedNEVRA := fmt.Sprintf("%s-%s:%s-%s.%s", value.Name, value.Epoch, value.Version, + value.Release, value.Architecture) + if value.NEVRA != wantedNEVRA { + return fmt.Errorf("repository package NEVRA mismatch for %q", value.FileName) + } + if !sha256Pattern.MatchString(value.SHA256) { + return fmt.Errorf("repository package %q digest is invalid", value.NEVRA) + } + if value.Source != request.RepositoryPackageSourceSync && value.Source != request.RepositoryPackageSourceUpload { + return fmt.Errorf("repository package %q source is invalid", value.NEVRA) + } + if (value.Source == request.RepositoryPackageSourceUpload) != (uuid.Validate(value.SourceUploadID) == nil) { + return fmt.Errorf("repository package %q upload identity is invalid", value.NEVRA) + } + if value.SignatureStatus != "" && value.SignatureStatus != request.RepositoryPackageSignatureUnchecked && + value.SignatureStatus != request.RepositoryPackageSignatureValid && + value.SignatureStatus != request.RepositoryPackageSignatureUnsigned && + value.SignatureStatus != request.RepositoryPackageSignatureUnauthorized && + value.SignatureStatus != request.RepositoryPackageSignatureBad { + return fmt.Errorf("repository package %q signature status is invalid", value.NEVRA) + } + if utf8.RuneCountInString(value.SignerKeyID) > 64 || utf8.RuneCountInString(value.SignatureMessage) > 1024 || + strings.IndexFunc(value.SignerKeyID, unicode.IsControl) >= 0 || + strings.IndexFunc(value.SignatureMessage, unicode.IsControl) >= 0 { + return fmt.Errorf("repository package %q signature detail is invalid", value.NEVRA) + } + if !safeName(value.FileName) || !safeRelativePath(value.RelativePath) || + path.Dir(value.RelativePath) != revision.RelativePath || !strings.HasSuffix(strings.ToLower(value.RelativePath), ".rpm") { + return fmt.Errorf("repository package %q path is invalid", value.NEVRA) + } + return nil +} + +func validateAggregate(document *Document, expected Expectation) error { + if len(document.Packages) != document.Revision.PackageCount { + return errors.New("repository result package count mismatch") + } + if expected.PackageCount >= 0 && expected.PackageCount != int64(document.Revision.PackageCount) { + return errors.New("repository result does not match prepared package count") + } + manifest, total := packageManifest(document.Packages) + if total != document.Revision.TotalBytes || manifest != document.Revision.ManifestSHA256 { + return errors.New("repository result package aggregate digest mismatch") + } + if expected.TotalBytes >= 0 && expected.TotalBytes != total { + return errors.New("repository result does not match prepared total bytes") + } + if len(document.Quarantine) != document.Revision.RejectedCount { + return errors.New("repository result quarantine count mismatch") + } + return nil +} + +func packageManifest(packages []PackageRecord) (string, int64) { + lines := make([]string, 0, len(packages)) + var total int64 + for _, item := range packages { + lines = append(lines, fmt.Sprintf("%s\t%s\t%d", item.NEVRA, item.SHA256, item.SizeBytes)) + total += item.SizeBytes + } + sort.Strings(lines) + digest := sha256.Sum256([]byte(strings.Join(lines, "\n") + "\n")) + return hex.EncodeToString(digest[:]), total +} + +func safeName(value string) bool { + return value != "" && value == path.Base(value) && value != "." && value != ".." && !strings.Contains(value, "\\") +} + +func safeRelativePath(value string) bool { + return value != "" && !strings.Contains(value, "\\") && !strings.HasPrefix(value, "/") && + path.Clean(value) == value && value != "." && !strings.HasPrefix(value, "../") +} diff --git a/manager/backend/services/footstone/infrastructure/repository/taskresult/result_test.go b/manager/backend/services/footstone/infrastructure/repository/taskresult/result_test.go new file mode 100644 index 0000000000000000000000000000000000000000..38ef1fde613861f87ef4da325a04bf9c8ae35c7c --- /dev/null +++ b/manager/backend/services/footstone/infrastructure/repository/taskresult/result_test.go @@ -0,0 +1,172 @@ +package taskresult + +import ( + "bytes" + "strings" + "testing" + + "github.com/klauspost/compress/zstd" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +const ( + testTaskID = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" + testRepositoryID = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" + testNodeID = "cccccccc-cccc-4ccc-8ccc-cccccccccccc" + testRevisionID = "dddddddd-dddd-4ddd-8ddd-dddddddddddd" +) + +func TestWriteAndParseZstd(t *testing.T) { + data, expected := validResult(t) + document, err := ParseZstd(bytes.NewReader(data), expected) + if err != nil { + t.Fatal(err) + } + if document.Revision.RevisionID != testRevisionID || len(document.Packages) != 1 || + document.Packages[0].Name != "ocm-sync-only" { + t.Fatalf("parsed document = %#v", document) + } +} + +func TestWriteAndParsePartialUploadWithQuarantine(t *testing.T) { + pkg := po.RepositoryPackage{RepositoryID: testRepositoryID, RevisionID: testRevisionID, + Name: "private-app", Epoch: "0", Version: "1.0", Release: "1", Architecture: "noarch", + NEVRA: "private-app-0:1.0-1.noarch", FileName: "private-app.rpm", + RelativePath: "public/private/revisions/" + testRevisionID + "/private-app.rpm", + SizeBytes: 7, SHA256: strings.Repeat("4", 64), Source: request.RepositoryPackageSourceUpload, + SourceUploadID: "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee"} + manifest, total := packageManifest([]PackageRecord{{NEVRA: pkg.NEVRA, SHA256: pkg.SHA256, + SizeBytes: pkg.SizeBytes}}) + revision := &po.RepositoryRevision{ID: testRevisionID, RepositoryID: testRepositoryID, + StorageNodeID: testNodeID, Kind: request.RepositoryRevisionKindUpload, PackageCount: 1, + TotalBytes: total, ManifestSHA256: manifest, RepomdSHA256: strings.Repeat("2", 64), + SnapshotManifestSHA256: strings.Repeat("3", 64), + RelativePath: "public/private/revisions/" + testRevisionID} + quarantine := po.RepositoryQuarantineItem{RepositoryID: testRepositoryID, TaskID: testTaskID, + UploadID: "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee", OriginalName: "damaged.rpm", + RelativePath: "quarantine/" + testRepositoryID + "/" + testTaskID + "/damaged.rpm", + SizeBytes: 5, SHA256: strings.Repeat("5", 64), ReasonCode: request.RepositoryQuarantineDamagedRPM, + ReasonMessage: "rpm integrity verification failed"} + var output bytes.Buffer + err := WriteZstdWithOptions(&output, &po.RepositoryTask{ID: testTaskID}, revision, + []po.RepositoryPackage{pkg}, WriteOptions{CompletionStatus: request.RepositoryTaskStatusPartialSuccess, + AcceptedCount: 1, RejectedCount: 1, ErrorCode: "SOME_PACKAGES_REJECTED", + ErrorMessage: "accepted=1 rejected=1", Quarantine: []po.RepositoryQuarantineItem{quarantine}}) + if err != nil { + t.Fatal(err) + } + document, err := ParseZstd(bytes.NewReader(output.Bytes()), Expectation{TaskID: testTaskID, + RepositoryID: testRepositoryID, StorageNodeID: testNodeID, RevisionID: testRevisionID, + PublishKey: "private", ManifestSHA256: revision.SnapshotManifestSHA256, + Kind: request.RepositoryRevisionKindUpload, Architecture: "x86_64", PackageCount: 1, + TotalBytes: total, CompletionStatus: request.RepositoryTaskStatusPartialSuccess, + UploadID: "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee"}) + if err != nil { + t.Fatal(err) + } + if document.Revision.AcceptedCount != 1 || document.Revision.RejectedCount != 1 || + len(document.Quarantine) != 1 || document.Quarantine[0].ReasonCode != request.RepositoryQuarantineDamagedRPM { + t.Fatalf("parsed partial document = %#v", document) + } +} + +func TestParseZstdRejectsUntrustedResults(t *testing.T) { + data, expected := validResult(t) + tests := []struct { + name string + change func(*Expectation) + want string + }{ + {name: "identity", change: func(value *Expectation) { value.TaskID = testRepositoryID }, want: "task identity mismatch"}, + {name: "manifest", change: func(value *Expectation) { value.ManifestSHA256 = strings.Repeat("f", 64) }, want: "snapshot manifest digest mismatch"}, + {name: "count", change: func(value *Expectation) { value.PackageCount = 2 }, want: "prepared package count"}, + {name: "bytes", change: func(value *Expectation) { value.TotalBytes = 8 }, want: "prepared total bytes"}, + {name: "expanded limit", change: func(value *Expectation) { value.MaxExpandedBytes = 32 }, want: "configured limit"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + value := expected + test.change(&value) + _, err := ParseZstd(bytes.NewReader(data), value) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("ParseZstd() error = %v, want substring %q", err, test.want) + } + }) + } +} + +func TestParseZstdRejectsUnknownFieldsAndTraversal(t *testing.T) { + for _, test := range []struct { + name, packagePath string + unknown bool + want string + }{ + {name: "unknown", unknown: true, packagePath: "public/oc9/revisions/" + testRevisionID + "/fixture.rpm", want: "unknown field"}, + {name: "traversal", packagePath: "public/oc9/revisions/" + testRevisionID + "/../fixture.rpm", want: "path is invalid"}, + } { + t.Run(test.name, func(t *testing.T) { + manifest := strings.Repeat("1", 64) + header := `{"record_type":"revision","schema_version":1,"task_id":"` + testTaskID + + `","repository_id":"` + testRepositoryID + `","storage_node_id":"` + testNodeID + + `","revision_id":"` + testRevisionID + `","kind":"SYNC","package_count":1,"total_bytes":7,` + + `"manifest_sha256":"` + manifest + `","repomd_sha256":"` + strings.Repeat("2", 64) + + `","snapshot_manifest_sha256":"` + strings.Repeat("3", 64) + `","relative_path":"public/oc9/revisions/` + testRevisionID + + `","completion_status":"SUCCESS","accepted_count":1,"rejected_count":0}` + if test.unknown { + header = strings.TrimSuffix(header, "}") + `,"unexpected":true}` + } + pkg := `{"record_type":"package","repository_id":"` + testRepositoryID + `","revision_id":"` + + testRevisionID + `","name":"fixture","epoch":"0","version":"1","release":"1","architecture":"x86_64",` + + `"nevra":"fixture-0:1-1.x86_64","file_name":"fixture.rpm","relative_path":"` + test.packagePath + + `","size_bytes":7,"sha256":"` + strings.Repeat("4", 64) + `","source":"SYNC"}` + data := compressRaw(t, header+"\n"+pkg+"\n") + _, err := ParseZstd(bytes.NewReader(data), Expectation{TaskID: testTaskID, RepositoryID: testRepositoryID, + StorageNodeID: testNodeID, RevisionID: testRevisionID, PublishKey: "oc9", ManifestSHA256: strings.Repeat("3", 64), + Kind: request.RepositoryRevisionKindSync, Architecture: "x86_64", PackageCount: 1, TotalBytes: 7}) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("ParseZstd() error = %v, want substring %q", err, test.want) + } + }) + } +} + +func validResult(t *testing.T) ([]byte, Expectation) { + t.Helper() + pkg := po.RepositoryPackage{RepositoryID: testRepositoryID, RevisionID: testRevisionID, + Name: "ocm-sync-only", Epoch: "0", Version: "1.0", Release: "1", Architecture: "x86_64", + NEVRA: "ocm-sync-only-0:1.0-1.x86_64", FileName: "ocm-sync-only.rpm", + RelativePath: "public/oc9/revisions/" + testRevisionID + "/0123456789abcdef-ocm-sync-only.rpm", + SizeBytes: 7, SHA256: strings.Repeat("4", 64), Source: request.RepositoryPackageSourceSync} + manifest, total := packageManifest([]PackageRecord{{NEVRA: pkg.NEVRA, SHA256: pkg.SHA256, SizeBytes: pkg.SizeBytes}}) + revision := &po.RepositoryRevision{ID: testRevisionID, RepositoryID: testRepositoryID, + StorageNodeID: testNodeID, Kind: request.RepositoryRevisionKindSync, PackageCount: 1, TotalBytes: total, + ManifestSHA256: manifest, RepomdSHA256: strings.Repeat("2", 64), + SnapshotManifestSHA256: strings.Repeat("3", 64), RelativePath: "public/oc9/revisions/" + testRevisionID} + task := &po.RepositoryTask{ID: testTaskID} + var output bytes.Buffer + if err := WriteZstd(&output, task, revision, []po.RepositoryPackage{pkg}); err != nil { + t.Fatal(err) + } + return output.Bytes(), Expectation{TaskID: testTaskID, RepositoryID: testRepositoryID, + StorageNodeID: testNodeID, RevisionID: testRevisionID, PublishKey: "oc9", + ManifestSHA256: revision.SnapshotManifestSHA256, Kind: request.RepositoryRevisionKindSync, + Architecture: "x86_64", PackageCount: 1, TotalBytes: total} +} + +func compressRaw(t *testing.T, value string) []byte { + t.Helper() + var output bytes.Buffer + writer, err := zstd.NewWriter(&output) + if err != nil { + t.Fatal(err) + } + if _, err = writer.Write([]byte(value)); err != nil { + t.Fatal(err) + } + if err = writer.Close(); err != nil { + t.Fatal(err) + } + return output.Bytes() +} diff --git a/manager/backend/services/footstone/infrastructure/repository/tmsclient/client.go b/manager/backend/services/footstone/infrastructure/repository/tmsclient/client.go new file mode 100644 index 0000000000000000000000000000000000000000..6e21bf96078c3c0196a1e7641e944b8d75446e47 --- /dev/null +++ b/manager/backend/services/footstone/infrastructure/repository/tmsclient/client.go @@ -0,0 +1,336 @@ +package tmsclient + +import ( + "bytes" + "context" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/url" + "os" + "sort" + "strconv" + "strings" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" +) + +type TargetResult struct { + AgentID uint64 + Status string + AttemptCount int + ExitCode int32 + StartedAt int64 + EndedAt int64 + Output string + OutputRef string + OutputTruncated bool + ErrorCode string + ErrorMessage string +} + +type TaskDetail struct { + TaskID string + Status string + Targets []TargetResult +} + +type Client interface { + ListNodes(context.Context) ([]request.RepositoryDistributionNode, error) + UploadArtifact(context.Context, string, []byte) (string, error) + Publish(context.Context, string, []string, int) (string, error) + PublishWithArgs(context.Context, string, string, []string, int) (string, error) + Detail(context.Context, string) (TaskDetail, error) + Cancel(context.Context, string, string) error +} + +type HTTPClient struct { + baseURL string + userName string + gatewayHMACSecret string + client *http.Client +} + +func New(baseURL, userName string) *HTTPClient { + return &HTTPClient{ + baseURL: strings.TrimRight(strings.TrimSpace(baseURL), "/"), + userName: strings.TrimSpace(userName), + gatewayHMACSecret: strings.TrimSpace(os.Getenv("TMS_API_GW_HMAC_SECRET")), + client: &http.Client{Timeout: 30 * time.Second}, + } +} + +func (c *HTTPClient) ListNodes(ctx context.Context) ([]request.RepositoryDistributionNode, error) { + var assets struct { + Items []struct { + AgentID uint64 `json:"agent_id"` + IP string `json:"ip"` + Status string `json:"status"` + } `json:"items"` + } + if err := c.postJSON(ctx, "/api/v1/assets/query", map[string]uint64{"agent_id": 0}, &assets); err != nil { + return nil, err + } + ids := make([]uint64, 0, len(assets.Items)) + for _, item := range assets.Items { + if item.AgentID != 0 { + ids = append(ids, item.AgentID) + } + } + statusByID := make(map[uint64]struct { + Connected bool + LastHeartbeat int64 + }) + for offset := 0; offset < len(ids); offset += 200 { + end := offset + 200 + if end > len(ids) { + end = len(ids) + } + var statuses struct { + Items []struct { + AgentID uint64 `json:"agent_id"` + Connected bool `json:"connected"` + LastHeartbeat int64 `json:"last_heartbeat_time"` + } `json:"items"` + } + if err := c.postJSON(ctx, "/api/v1/agents/status", map[string]interface{}{"agent_ids": ids[offset:end]}, &statuses); err != nil { + return nil, err + } + for _, item := range statuses.Items { + statusByID[item.AgentID] = struct { + Connected bool + LastHeartbeat int64 + }{item.Connected, item.LastHeartbeat} + } + } + result := make([]request.RepositoryDistributionNode, 0, len(assets.Items)) + for _, item := range assets.Items { + status := statusByID[item.AgentID] + result = append(result, request.RepositoryDistributionNode{ + AgentID: item.AgentID, IP: item.IP, AssetStatus: item.Status, + Connected: status.Connected, LastHeartbeatAt: status.LastHeartbeat, + }) + } + sort.Slice(result, func(i, j int) bool { return result[i].IP < result[j].IP }) + return result, nil +} + +func (c *HTTPClient) UploadArtifact(ctx context.Context, name string, content []byte) (string, error) { + var body bytes.Buffer + writer := multipart.NewWriter(&body) + part, err := writer.CreateFormFile("file", name) + if err != nil { + return "", err + } + if _, err = part.Write(content); err != nil { + return "", err + } + if err = writer.Close(); err != nil { + return "", err + } + requestValue, err := c.newRequest(ctx, http.MethodPost, "/api/v1/artifacts/upload", &body) + if err != nil { + return "", err + } + requestValue.Header.Set("Content-Type", writer.FormDataContentType()) + var data struct { + MD5 string `json:"md5"` + } + if err = c.do(requestValue, &data); err != nil { + return "", err + } + if len(data.MD5) != 32 { + return "", fmt.Errorf("tms artifact upload returned an invalid md5") + } + return data.MD5, nil +} + +func (c *HTTPClient) Publish(ctx context.Context, artifactMD5 string, ips []string, timeout int) (string, error) { + return c.PublishWithArgs(ctx, artifactMD5, "", ips, timeout) +} + +// PublishWithArgs keeps the existing RUN_ARTIFACT transport while allowing a +// caller to pass a short-lived, base64-encoded bootstrap payload to run.sh. +func (c *HTTPClient) PublishWithArgs(ctx context.Context, artifactMD5, argsB64 string, + ips []string, timeout int, +) (string, error) { + var data struct { + TaskID string `json:"task_id"` + Accepted int `json:"accepted_target_count"` + } + err := c.postJSON(ctx, "/api/v1/ops_tasks/publish", map[string]interface{}{ + "artifact_md5": artifactMD5, + "args_b64": argsB64, + "timeout_seconds": timeout, + "targets": map[string]interface{}{"ips": ips}, + }, &data) + if err != nil { + return "", err + } + if data.TaskID == "" || data.Accepted != len(ips) { + return "", fmt.Errorf("tms accepted %d of %d distribution targets", data.Accepted, len(ips)) + } + return data.TaskID, nil +} + +func (c *HTTPClient) Detail(ctx context.Context, taskID string) (TaskDetail, error) { + var data struct { + Task struct { + TaskID string `json:"task_id"` + Status string `json:"deploy_status"` + } `json:"task"` + Targets []struct { + AgentID uint64 `json:"agent_id"` + Status string `json:"deploy_status"` + AttemptCount int `json:"attempt_count"` + ExitCode int32 `json:"exit_code"` + StartedAt int64 `json:"started_at"` + EndedAt int64 `json:"ended_at"` + Output string `json:"output"` + OutputPreview string `json:"output_preview"` + OutputRef string `json:"output_ref"` + OutputTruncated bool `json:"output_truncated"` + ErrorCode json.Number `json:"error_code"` + ErrorMessage string `json:"error_message"` + } `json:"targets"` + } + if err := c.postJSON(ctx, "/api/v1/ops_tasks/detail", map[string]interface{}{ + "task_id": taskID, "page": 1, "page_size": 200, + }, &data); err != nil { + return TaskDetail{}, err + } + result := TaskDetail{TaskID: data.Task.TaskID, Status: normalizeStatus(data.Task.Status)} + for _, target := range data.Targets { + output := target.Output + if output == "" { + output = target.OutputPreview + } + result.Targets = append(result.Targets, TargetResult{ + AgentID: target.AgentID, Status: normalizeStatus(target.Status), AttemptCount: target.AttemptCount, + ExitCode: target.ExitCode, StartedAt: target.StartedAt, EndedAt: target.EndedAt, + Output: output, OutputRef: target.OutputRef, OutputTruncated: target.OutputTruncated, + ErrorCode: string(target.ErrorCode), ErrorMessage: target.ErrorMessage, + }) + } + return result, nil +} + +func (c *HTTPClient) Cancel(ctx context.Context, taskID, reason string) error { + var ignored map[string]interface{} + return c.postJSON(ctx, "/api/v1/ops_tasks/cancel", map[string]string{"task_id": taskID, "reason": reason}, &ignored) +} + +func (c *HTTPClient) postJSON(ctx context.Context, path string, input, output interface{}) error { + body, err := json.Marshal(input) + if err != nil { + return err + } + req, err := c.newRequest(ctx, http.MethodPost, path, bytes.NewReader(body)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + return c.do(req, output) +} + +func (c *HTTPClient) newRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) { + if c.baseURL == "" { + return nil, fmt.Errorf("repository tms_base_url is not configured") + } + parsed, err := url.Parse(c.baseURL) + if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" { + return nil, fmt.Errorf("repository tms_base_url is invalid") + } + req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, body) + if err == nil { + req.Header.Set("X-User-Name", c.userName) + req.Header.Set("X-User-Role", "user") + err = c.signGatewayRequest(req) + } + return req, err +} + +func (c *HTTPClient) signGatewayRequest(req *http.Request) error { + if c.gatewayHMACSecret == "" { + return nil + } + body := []byte{} + if req.GetBody != nil { + reader, err := req.GetBody() + if err != nil { + return fmt.Errorf("read tms request body for signing: %w", err) + } + defer reader.Close() + body, err = io.ReadAll(reader) + if err != nil { + return fmt.Errorf("read tms request body for signing: %w", err) + } + } else if req.Body != nil { + return fmt.Errorf("tms request body cannot be replayed for signing") + } + + nonceBytes := make([]byte, 16) + if _, err := rand.Read(nonceBytes); err != nil { + return fmt.Errorf("generate tms request nonce: %w", err) + } + timestamp := strconv.FormatInt(time.Now().Unix(), 10) + nonce := hex.EncodeToString(nonceBytes) + bodyDigest := sha256.Sum256(body) + canonical := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n%s", req.Method, req.URL.Path, + c.userName, "user", timestamp, nonce, hex.EncodeToString(bodyDigest[:])) + mac := hmac.New(sha256.New, []byte(c.gatewayHMACSecret)) + _, _ = mac.Write([]byte(canonical)) + + req.Header.Set("X-Signature-Timestamp", timestamp) + req.Header.Set("X-Signature-Nonce", nonce) + req.Header.Set("X-Signature", hex.EncodeToString(mac.Sum(nil))) + return nil +} + +func (c *HTTPClient) do(req *http.Request, output interface{}) error { + response, err := c.client.Do(req) + if err != nil { + return fmt.Errorf("tms request failed: %w", err) + } + defer response.Body.Close() + body, err := io.ReadAll(io.LimitReader(response.Body, 4<<20)) + if err != nil { + return err + } + var envelope struct { + Code string `json:"code"` + Message string `json:"message"` + Data json.RawMessage `json:"data"` + } + if err = json.Unmarshal(body, &envelope); err != nil { + return fmt.Errorf("tms returned invalid json: %w", err) + } + if response.StatusCode < 200 || response.StatusCode >= 300 || envelope.Code != "OK" { + return fmt.Errorf("tms %s: %s", envelope.Code, envelope.Message) + } + if output != nil && len(envelope.Data) > 0 { + if err = json.Unmarshal(envelope.Data, output); err != nil { + return err + } + } + return nil +} + +func normalizeStatus(value string) string { + value = strings.TrimPrefix(strings.TrimSpace(value), "TASK_") + value = strings.TrimPrefix(value, "TARGET_") + if value == "ACKED_SUCCESS" { + return "SUCCESS" + } + if value == "ACKED_FAILED" { + return "FAILED" + } + return value +} diff --git a/manager/backend/services/footstone/infrastructure/repository/tmsclient/client_test.go b/manager/backend/services/footstone/infrastructure/repository/tmsclient/client_test.go new file mode 100644 index 0000000000000000000000000000000000000000..461ffe0312c2e3f5d7f77764bc51bca395c4da10 --- /dev/null +++ b/manager/backend/services/footstone/infrastructure/repository/tmsclient/client_test.go @@ -0,0 +1,159 @@ +package tmsclient + +import ( + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestListNodesJoinsAssetsAndConnectionStatus(t *testing.T) { + t.Setenv("TMS_API_GW_HMAC_SECRET", "") + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch r.URL.Path { + case "/api/v1/assets/query": + var body map[string]uint64 + _ = json.NewDecoder(r.Body).Decode(&body) + if body["agent_id"] != 0 { + t.Errorf("agent_id = %d", body["agent_id"]) + } + _, _ = w.Write([]byte(`{"code":"OK","data":{"items":[` + + `{"agent_id":5902982880214862284,"ip":"192.0.2.2","status":"allowed"},` + + `{"agent_id":3761443979690029541,"ip":"192.0.2.1","status":"allowed"}]}}`)) + case "/api/v1/agents/status": + _, _ = w.Write([]byte(`{"code":"OK","data":{"items":[` + + `{"agent_id":5902982880214862284,"connected":true,"last_heartbeat_time":123},` + + `{"agent_id":3761443979690029541,"connected":false}]}}`)) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + client := New(server.URL, "repository-test") + nodes, err := client.ListNodes(context.Background()) + if err != nil { + t.Fatal(err) + } + if len(nodes) != 2 || nodes[0].IP != "192.0.2.1" || nodes[1].AgentID != 5902982880214862284 || + !nodes[1].Connected || nodes[1].LastHeartbeatAt != 123 { + t.Fatalf("nodes = %#v", nodes) + } +} + +func TestDetailNormalizesTMSStatuses(t *testing.T) { + t.Setenv("TMS_API_GW_HMAC_SECRET", "") + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"code":"OK","data":{"task":{"task_id":"t-1","deploy_status":"TASK_PARTIAL_SUCCESS"},` + + `"targets":[{"agent_id":5902982880214862284,"deploy_status":"TARGET_ACKED_SUCCESS","output":"ok"},` + + `{"agent_id":3761443979690029541,"deploy_status":"FAILED","exit_code":17,"error_code":31}]}}`)) + })) + defer server.Close() + detail, err := New(server.URL, "repository-test").Detail(context.Background(), "t-1") + if err != nil { + t.Fatal(err) + } + if detail.Status != "PARTIAL_SUCCESS" || detail.Targets[0].Status != "SUCCESS" || + detail.Targets[1].ExitCode != 17 || detail.Targets[1].ErrorCode != "31" { + t.Fatalf("detail = %#v", detail) + } +} + +func TestPublishSignsRequestForStrictTMS(t *testing.T) { + const secret = "repository-tms-test-secret" + t.Setenv("TMS_API_GW_HMAC_SECRET", secret) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + timestamp := r.Header.Get("X-Signature-Timestamp") + nonce := r.Header.Get("X-Signature-Nonce") + digest := sha256.Sum256(body) + canonical := strings.Join([]string{r.Method, r.URL.Path, "repository-test", "user", timestamp, + nonce, hex.EncodeToString(digest[:])}, "\n") + mac := hmac.New(sha256.New, []byte(secret)) + _, _ = mac.Write([]byte(canonical)) + if timestamp == "" || nonce == "" || + !hmac.Equal([]byte(r.Header.Get("X-Signature")), []byte(hex.EncodeToString(mac.Sum(nil)))) { + t.Fatalf("request is missing a valid gateway signature") + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"code":"OK","data":{"task_id":"t-1","accepted_target_count":1}}`)) + })) + defer server.Close() + + taskID, err := New(server.URL, "repository-test").Publish(context.Background(), strings.Repeat("a", 32), + []string{"192.0.2.1"}, 60) + if err != nil || taskID != "t-1" { + t.Fatalf("Publish() = %q, %v", taskID, err) + } +} + +func TestPublishWithArgsPassesBootstrapPayloadUnchanged(t *testing.T) { + t.Setenv("TMS_API_GW_HMAC_SECRET", "") + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var body struct { + ArtifactMD5 string `json:"artifact_md5"` + ArgsB64 string `json:"args_b64"` + Targets struct { + IPs []string `json:"ips"` + } `json:"targets"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatal(err) + } + if body.ArtifactMD5 != strings.Repeat("a", 32) || body.ArgsB64 != "Ym9vdHN0cmFw" || + len(body.Targets.IPs) != 1 || body.Targets.IPs[0] != "192.0.2.1" { + t.Fatalf("publish body = %#v", body) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"code":"OK","data":{"task_id":"t-bootstrap","accepted_target_count":1}}`)) + })) + defer server.Close() + + taskID, err := New(server.URL, "repository-test").PublishWithArgs(context.Background(), + strings.Repeat("a", 32), "Ym9vdHN0cmFw", []string{"192.0.2.1"}, 300) + if err != nil || taskID != "t-bootstrap" { + t.Fatalf("PublishWithArgs() = %q, %v", taskID, err) + } +} + +func TestUploadArtifactSignsMultipartBodyForStrictTMS(t *testing.T) { + const secret = "repository-tms-upload-test-secret" + t.Setenv("TMS_API_GW_HMAC_SECRET", secret) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + timestamp := r.Header.Get("X-Signature-Timestamp") + nonce := r.Header.Get("X-Signature-Nonce") + digest := sha256.Sum256(body) + canonical := strings.Join([]string{r.Method, r.URL.Path, "repository-test", "user", timestamp, + nonce, hex.EncodeToString(digest[:])}, "\n") + mac := hmac.New(sha256.New, []byte(secret)) + _, _ = mac.Write([]byte(canonical)) + if !strings.HasPrefix(r.Header.Get("Content-Type"), "multipart/form-data;") || timestamp == "" || + nonce == "" || !hmac.Equal([]byte(r.Header.Get("X-Signature")), + []byte(hex.EncodeToString(mac.Sum(nil)))) { + t.Fatalf("multipart request is missing a valid gateway signature") + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"code":"OK","data":{"md5":"0123456789abcdef0123456789abcdef"}}`)) + })) + defer server.Close() + + md5, err := New(server.URL, "repository-test").UploadArtifact(context.Background(), "revision.tar.gz", + []byte("immutable-revision")) + if err != nil || md5 != "0123456789abcdef0123456789abcdef" { + t.Fatalf("UploadArtifact() = %q, %v", md5, err) + } +} diff --git a/manager/backend/services/footstone/infrastructure/trpc/http.go b/manager/backend/services/footstone/infrastructure/trpc/http.go index 7d1ed63dc8992ffcf17d454cf25b68cff6725a32..3bb0fc782a0baebddb66134e877b2f38473b779a 100644 --- a/manager/backend/services/footstone/infrastructure/trpc/http.go +++ b/manager/backend/services/footstone/infrastructure/trpc/http.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" stdhttp "net/http" + "strings" "trpc.group/trpc-go/trpc-go/errs" "trpc.group/trpc-go/trpc-go/log" @@ -20,6 +21,26 @@ type Response struct { Data json.RawMessage `json:"data"` } +// businessErrorCode extracts an explicitly declared stable code from a +// business error formatted as "ERROR_CODE: user-facing message". Existing +// unprefixed errors keep the historical InternalError response code. +func businessErrorCode(msg string) (string, string) { + prefix, detail, found := strings.Cut(msg, ":") + if !found || len(prefix) < 3 || len(prefix) > 64 { + return "InternalError", msg + } + for _, value := range prefix { + if (value < 'A' || value > 'Z') && (value < '0' || value > '9') && value != '_' { + return "InternalError", msg + } + } + detail = strings.TrimSpace(detail) + if detail == "" { + return "InternalError", msg + } + return prefix, detail +} + // DefaultHttpRespHandler default trpc http handler var DefaultHttpRespHandler = func(w stdhttp.ResponseWriter, r *stdhttp.Request, rspbody []byte) (err error) { var data json.RawMessage @@ -63,16 +84,19 @@ var DefaultErrHandler = func(w stdhttp.ResponseWriter, r *stdhttp.Request, e *er // 业务错误面向用户,原样返回;框架错误和 validation 保留错误码脱敏后返回通用文案,详细错误仅记录服务端日志, // 避免向前端泄露 proto 字段名 / rpc 服务名 / 内网拓扑等实现细节。 msg := e.Msg + errorCode := "InternalError" if e != nil && (e.Type == errs.ErrorTypeFramework || e.Type == errs.ErrorTypeCalleeFramework || e.Code == errs.RetServerValidateFail) { log.Errorf("footstone http sanitized error: type=%d code=%d msg=%s", e.Type, e.Code, e.Msg) msg = sanitizeFrameworkMsg(e) + } else { + errorCode, msg = businessErrorCode(msg) } // 填充指定格式错误信息到HTTP Body resp := Response{ Success: false, ErrorMsg: msg, - ErrorCode: "InternalError", + ErrorCode: errorCode, Data: nil, ReturnCode: int(e.Code), } diff --git a/manager/backend/services/footstone/infrastructure/trpc/http_test.go b/manager/backend/services/footstone/infrastructure/trpc/http_test.go index 72002cc2f24aa24c49b84049a7fcdc479f1be9d1..d065a7961b893eb3a1a3f9467d6f791e3d23fa4b 100644 --- a/manager/backend/services/footstone/infrastructure/trpc/http_test.go +++ b/manager/backend/services/footstone/infrastructure/trpc/http_test.go @@ -34,6 +34,33 @@ func TestDefaultErrHandlerPreservesBusinessErrorMessages(t *testing.T) { if response.ErrorMsg != message { t.Errorf("ErrorMsg = %q, want %q", response.ErrorMsg, message) } + if response.ErrorCode != "InternalError" { + t.Errorf("ErrorCode = %q, want InternalError", response.ErrorCode) + } +} + +func TestDefaultErrHandlerExtractsStableBusinessErrorCode(t *testing.T) { + businessErr := errs.New(100001, "REVISION_CHANGED: repository revision changed after preview").(*errs.Error) + + response := recordErrorResponse(t, businessErr) + if response.ErrorCode != "REVISION_CHANGED" { + t.Errorf("ErrorCode = %q, want REVISION_CHANGED", response.ErrorCode) + } + if response.ErrorMsg != "repository revision changed after preview" { + t.Errorf("ErrorMsg = %q", response.ErrorMsg) + } +} + +func TestDefaultErrHandlerRejectsInvalidBusinessErrorPrefix(t *testing.T) { + businessErr := errs.New(100001, "mixed-prefix: keep this message").(*errs.Error) + + response := recordErrorResponse(t, businessErr) + if response.ErrorCode != "InternalError" { + t.Errorf("ErrorCode = %q, want InternalError", response.ErrorCode) + } + if response.ErrorMsg != businessErr.Msg { + t.Errorf("ErrorMsg = %q, want %q", response.ErrorMsg, businessErr.Msg) + } } func recordErrorResponse(t *testing.T, trpcErr *errs.Error) Response { diff --git a/manager/backend/services/footstone/presentation/permission_controller.go b/manager/backend/services/footstone/presentation/permission_controller.go index d17aedac9cdfd66974be4a56f409936958b1af76..b66d1a8d2097e5a39d51b88f55ea683b04937b23 100644 --- a/manager/backend/services/footstone/presentation/permission_controller.go +++ b/manager/backend/services/footstone/presentation/permission_controller.go @@ -123,11 +123,25 @@ func (p *PermissionController) ListMenus(_ context.Context, _ *ftpb.ListMenusReq Type: ftpb.PermissionType_CommonType, }, }, + { + Menu: &ftpb.MenuInfo{ + Name: "软件仓库", + RoutePath: "/repository", + Weight: 5, + Type: ftpb.PermissionType_CommonType, + }, + SubMenu: []*ftpb.MenuTree{ + {Menu: &ftpb.MenuInfo{Name: "仓库管理", RoutePath: "/repository/list", Weight: 1, Type: ftpb.PermissionType_CommonType}}, + {Menu: &ftpb.MenuInfo{Name: "任务总览", RoutePath: "/repository/tasks", Weight: 2, Type: ftpb.PermissionType_CommonType}}, + {Menu: &ftpb.MenuInfo{Name: "仓库节点", RoutePath: "/repository/nodes", Weight: 3, Type: ftpb.PermissionType_CommonType}}, + {Menu: &ftpb.MenuInfo{Name: "上游源", RoutePath: "/repository/sources", Weight: 4, Type: ftpb.PermissionType_CommonType}}, + }, + }, { Menu: &ftpb.MenuInfo{ Name: "设置", RoutePath: "/setting", - Weight: 5, + Weight: 6, Type: ftpb.PermissionType_CommonType, }, SubMenu: []*ftpb.MenuTree{ diff --git a/manager/backend/services/footstone/presentation/permission_controller_repository_test.go b/manager/backend/services/footstone/presentation/permission_controller_repository_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d74a9e3ad2d3e6b059f67c6b675bc9a51657a2d0 --- /dev/null +++ b/manager/backend/services/footstone/presentation/permission_controller_repository_test.go @@ -0,0 +1,36 @@ +package presentation + +import ( + "context" + "testing" + + ftpb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/footstone" +) + +func TestListMenusIncludesRepositoryManagement(t *testing.T) { + response, err := (&PermissionController{}).ListMenus(context.Background(), &ftpb.ListMenusReq{}) + if err != nil { + t.Fatalf("ListMenus returned an error: %v", err) + } + + for _, menu := range response.GetMenus() { + if menu.GetMenu().GetRoutePath() != "/repository" { + continue + } + if menu.GetMenu().GetName() != "软件仓库" { + t.Fatalf("repository menu name = %q, want %q", menu.GetMenu().GetName(), "软件仓库") + } + if len(menu.GetSubMenu()) != 4 { + t.Fatalf("repository submenu count = %d, want 4", len(menu.GetSubMenu())) + } + want := []string{"/repository/list", "/repository/tasks", "/repository/nodes", "/repository/sources"} + for index, path := range want { + if got := menu.GetSubMenu()[index].GetMenu().GetRoutePath(); got != path { + t.Fatalf("repository submenu %d path = %q, want %q", index, got, path) + } + } + return + } + + t.Fatal("repository management menu is missing") +} diff --git a/manager/backend/services/footstone/presentation/repository_content_controller.go b/manager/backend/services/footstone/presentation/repository_content_controller.go new file mode 100644 index 0000000000000000000000000000000000000000..70afeabb42c1922fa3159a46ba62ad2ce6879a75 --- /dev/null +++ b/manager/backend/services/footstone/presentation/repository_content_controller.go @@ -0,0 +1,369 @@ +package presentation + +import ( + "context" + "fmt" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func (c *RepositoryController) SyncRepository(ctx context.Context, + req *repositorypb.SyncRepositoryRequest, +) (*repositorypb.RepositoryTaskResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + value, err := c.application.SyncRepository(c.ToContext(ctx), req.GetRepositoryId()) + if err != nil { + return nil, repositoryNodeRPMErrorForAPI(err) + } + return &repositorypb.RepositoryTaskResponse{Task: repositoryTaskToPB(value)}, nil +} + +func (c *RepositoryController) MigrateRepository(ctx context.Context, + req *repositorypb.MigrateRepositoryRequest, +) (*repositorypb.RepositoryTaskResponse, error) { + value, err := c.application.MigrateRepository(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryTaskResponse{Task: repositoryTaskToPB(value)}, nil +} + +func (c *RepositoryController) DeleteRepositoryGPGKey(ctx context.Context, + req *repositorypb.DeleteRepositoryGPGKeyRequest, +) (*emptypb.Empty, error) { + if err := c.application.DeleteRepositoryGPGKey(c.ToContext(ctx), req); err != nil { + return nil, err + } + return &emptypb.Empty{}, nil +} + +func (c *RepositoryController) GenerateRepositoryConfig(ctx context.Context, + req *repositorypb.GenerateRepositoryConfigRequest, +) (*repositorypb.RepositoryConfigResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + value, err := c.application.GenerateRepositoryConfig(c.ToContext(ctx), req.GetRepositoryId(), + req.GetImmutableRevision()) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryConfigResponse{Config: repositoryConfigToPB(value)}, nil +} + +func repositoryConfigToPB(value *request.GeneratedRepositoryConfig) *repositorypb.RepositoryConfig { + if value == nil { + return nil + } + return &repositorypb.RepositoryConfig{ + RepositoryId: value.RepositoryID, ActiveRevisionId: value.ActiveRevisionID, + RepoId: value.RepoID, FileName: value.FileName, ContentType: value.ContentType, + Content: value.Content, Baseurl: value.BaseURL, RevisionUrl: value.RevisionURL, + ImmutableRevision: value.ImmutableRevision, Enabled: value.Enabled, + Gpgcheck: value.GPGCheck, RepoGpgcheck: value.RepoGPGCheck, + GpgkeyUrls: value.GPGKeyURLs, + } +} + +func (c *RepositoryController) RegisterRepositoryGPGKey(ctx context.Context, + req *repositorypb.RegisterRepositoryGPGKeyRequest, +) (*repositorypb.RepositoryGPGKeyResponse, error) { + value, err := c.application.RegisterRepositoryGPGKey(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryGPGKeyResponse{Key: repositoryGPGKeyToPB(value)}, nil +} + +func (c *RepositoryController) ListRepositoryGPGKeys(ctx context.Context, + req *repositorypb.ListRepositoryGPGKeysRequest, +) (*repositorypb.ListRepositoryGPGKeysResponse, error) { + values, err := c.application.ListRepositoryGPGKeys(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + result := make([]*repositorypb.RepositoryGPGKey, 0, len(values)) + for index := range values { + result = append(result, repositoryGPGKeyToPB(&values[index])) + } + return &repositorypb.ListRepositoryGPGKeysResponse{Keys: result}, nil +} + +func repositoryGPGKeyToPB(value *request.RepositoryGPGKey) *repositorypb.RepositoryGPGKey { + if value == nil { + return nil + } + return &repositorypb.RepositoryGPGKey{ + Id: value.ID, RepositoryId: value.RepositoryID, KeyId: value.KeyID, + Fingerprint: value.Fingerprint, PublicKeyPath: value.PublicKeyPath, + PublicKeyUrl: value.PublicKeyURL, CreatedAt: timestamppb.New(value.CreatedAt), + } +} + +func (c *RepositoryController) BeginRepositoryUpload(ctx context.Context, + req *repositorypb.BeginRepositoryUploadRequest, +) (*repositorypb.RepositoryUploadResponse, error) { + value, err := c.application.BeginRepositoryUpload(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryUploadResponse{Upload: repositoryUploadToPB(value)}, nil +} + +func (c *RepositoryController) AppendRepositoryUploadChunk(ctx context.Context, + req *repositorypb.AppendRepositoryUploadChunkRequest, +) (*repositorypb.RepositoryUploadResponse, error) { + value, err := c.application.AppendRepositoryUploadChunk(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryUploadResponse{Upload: repositoryUploadToPB(value)}, nil +} + +func (c *RepositoryController) PutRepositoryUploadChunk(ctx context.Context, + req *repositorypb.PutRepositoryUploadChunkRequest, +) (*repositorypb.PutRepositoryUploadChunkResponse, error) { + upload, chunk, err := c.application.PutRepositoryUploadChunk(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + return &repositorypb.PutRepositoryUploadChunkResponse{ + Upload: repositoryUploadToPB(upload), Chunk: repositoryUploadChunkToPB(chunk), + }, nil +} + +func (c *RepositoryController) DescribeRepositoryUploadChunks(ctx context.Context, + req *repositorypb.DescribeRepositoryUploadRequest, +) (*repositorypb.DescribeRepositoryUploadChunksResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + upload, chunks, missing, err := c.application.DescribeRepositoryUploadChunks(c.ToContext(ctx), req.GetUploadId()) + if err != nil { + return nil, err + } + result := &repositorypb.DescribeRepositoryUploadChunksResponse{ + Upload: repositoryUploadToPB(upload), Chunks: make([]*repositorypb.RepositoryUploadChunk, 0, len(chunks)), + MissingChunkIndexes: make([]int32, 0, len(missing)), + } + for index := range chunks { + result.Chunks = append(result.Chunks, repositoryUploadChunkToPB(&chunks[index])) + } + for _, chunkIndex := range missing { + result.MissingChunkIndexes = append(result.MissingChunkIndexes, int32(chunkIndex)) + } + return result, nil +} + +func (c *RepositoryController) PauseRepositoryUpload(ctx context.Context, + req *repositorypb.DescribeRepositoryUploadRequest, +) (*repositorypb.RepositoryUploadResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + value, err := c.application.PauseRepositoryUpload(c.ToContext(ctx), req.GetUploadId()) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryUploadResponse{Upload: repositoryUploadToPB(value)}, nil +} + +func (c *RepositoryController) ResumeRepositoryUpload(ctx context.Context, + req *repositorypb.DescribeRepositoryUploadRequest, +) (*repositorypb.RepositoryUploadResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + value, err := c.application.ResumeRepositoryUpload(c.ToContext(ctx), req.GetUploadId()) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryUploadResponse{Upload: repositoryUploadToPB(value)}, nil +} + +func (c *RepositoryController) CompleteRepositoryUpload(ctx context.Context, + req *repositorypb.CompleteRepositoryUploadRequest, +) (*repositorypb.CompleteRepositoryUploadResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + upload, task, err := c.application.CompleteRepositoryUpload(c.ToContext(ctx), req.GetUploadId()) + if err != nil { + return nil, err + } + return &repositorypb.CompleteRepositoryUploadResponse{ + Upload: repositoryUploadToPB(upload), Task: repositoryTaskToPB(task), + }, nil +} + +func (c *RepositoryController) AbortRepositoryUpload(ctx context.Context, + req *repositorypb.AbortRepositoryUploadRequest, +) (*repositorypb.RepositoryUploadResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + value, err := c.application.AbortRepositoryUpload(c.ToContext(ctx), req.GetUploadId()) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryUploadResponse{Upload: repositoryUploadToPB(value)}, nil +} + +func (c *RepositoryController) DescribeRepositoryUpload(ctx context.Context, + req *repositorypb.DescribeRepositoryUploadRequest, +) (*repositorypb.RepositoryUploadResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + value, err := c.application.DescribeRepositoryUpload(c.ToContext(ctx), req.GetUploadId()) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryUploadResponse{Upload: repositoryUploadToPB(value)}, nil +} + +func (c *RepositoryController) ListRepositoryPackages(ctx context.Context, + req *repositorypb.ListRepositoryPackagesRequest, +) (*repositorypb.ListRepositoryPackagesResponse, error) { + values, revisionID, page, err := c.application.ListRepositoryPackages(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + response := &repositorypb.ListRepositoryPackagesResponse{ + RevisionId: revisionID, Packages: make([]*repositorypb.RepositoryPackage, 0, len(values)), + PageResponse: request.ToPageResponse(page), + } + for index := range values { + response.Packages = append(response.Packages, repositoryPackageToPB(&values[index])) + } + return response, nil +} + +func (c *RepositoryController) ListRepositoryQuarantine(ctx context.Context, + req *repositorypb.ListRepositoryQuarantineRequest, +) (*repositorypb.ListRepositoryQuarantineResponse, error) { + values, page, err := c.application.ListRepositoryQuarantine(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + response := &repositorypb.ListRepositoryQuarantineResponse{ + Items: make([]*repositorypb.RepositoryQuarantineItem, 0, len(values)), + PageResponse: request.ToPageResponse(page), + } + for index := range values { + response.Items = append(response.Items, repositoryQuarantineToPB(&values[index])) + } + return response, nil +} + +func (c *RepositoryController) DeleteRepositoryPackage(ctx context.Context, + req *repositorypb.DeleteRepositoryPackageRequest, +) (*repositorypb.RepositoryTaskResponse, error) { + value, err := c.application.DeleteRepositoryPackage(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryTaskResponse{Task: repositoryTaskToPB(value)}, nil +} + +func (c *RepositoryController) PreviewRepositoryPackageDeletion(ctx context.Context, + req *repositorypb.PreviewRepositoryPackageDeletionRequest, +) (*repositorypb.RepositoryPackageDeletionPreviewResponse, error) { + result, err := c.application.PreviewRepositoryPackageDeletion(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + preview := &repositorypb.RepositoryPackageDeletionPreview{ + PreviewId: result.Preview.ID, PreviewDigest: result.Preview.PreviewDigest, + RepositoryId: result.Payload.RepositoryID, RevisionId: result.Payload.RevisionID, + Nevras: result.Payload.NEVRAs, Command: result.Payload.Command, RawOutput: result.Payload.RawOutput, + Status: result.Preview.Status, ExpiresAt: timestamppb.New(result.Preview.ExpiresAt), + CreatedBy: result.Preview.CreatedBy, + RepositorySet: make([]*repositorypb.RepositoryPackageDeletionPreviewSource, 0, + len(result.Payload.RepositorySet)), + DependencyImpact: make([]*repositorypb.RepositoryPackageDependencyImpact, 0, + len(result.Payload.DependencyImpact)), + } + for _, source := range result.Payload.RepositorySet { + preview.RepositorySet = append(preview.RepositorySet, &repositorypb.RepositoryPackageDeletionPreviewSource{ + RepositoryId: source.RepositoryID, RevisionId: source.RevisionID, + RepoId: source.RepoID, Baseurl: source.BaseURL, + }) + } + for _, dependency := range result.Payload.DependencyImpact { + preview.DependencyImpact = append(preview.DependencyImpact, &repositorypb.RepositoryPackageDependencyImpact{ + PackageNevra: dependency.PackageNEVRA, Requires: dependency.Requires, + Depth: int32(dependency.Depth), Direct: dependency.Direct, Reason: dependency.Reason, + }) + } + return &repositorypb.RepositoryPackageDeletionPreviewResponse{Preview: preview}, nil +} + +func (c *RepositoryController) ConfirmRepositoryPackageDeletion(ctx context.Context, + req *repositorypb.ConfirmRepositoryPackageDeletionRequest, +) (*repositorypb.RepositoryTaskResponse, error) { + task, err := c.application.ConfirmRepositoryPackageDeletion(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryTaskResponse{Task: repositoryTaskToPB(task)}, nil +} + +func repositoryUploadToPB(value *po.RepositoryUpload) *repositorypb.RepositoryUpload { + if value == nil { + return nil + } + result := &repositorypb.RepositoryUpload{ + Id: value.ID, RepositoryId: value.RepositoryID, ProjectId: value.ProjectID, TaskId: value.TaskID, + StorageNodeId: value.StorageNodeID, ClientFingerprint: value.ClientFingerprint, + FileName: value.FileName, Kind: value.Kind, ExpectedSize: value.ExpectedSize, + ChunkSize: int32(value.ChunkSize), TotalChunks: int32(value.TotalChunks), + UploadedCount: int32(value.UploadedCount), + ReceivedSize: value.ReceivedSize, ExpectedSha256: value.ExpectedSHA256, ActualSha256: value.ActualSHA256, + Status: value.Status, ErrorCode: value.ErrorCode, ErrorMessage: value.ErrorMessage, + ExpiresAt: timestamppb.New(value.ExpiresAt), CreatedAt: timestamppb.New(value.CreatedAt), + UpdatedAt: timestamppb.New(value.UpdatedAt), + } + if value.CompletedAt != nil { + result.CompletedAt = timestamppb.New(*value.CompletedAt) + } + if value.PausedAt != nil { + result.PausedAt = timestamppb.New(*value.PausedAt) + } + return result +} + +func repositoryUploadChunkToPB(value *po.RepositoryUploadChunk) *repositorypb.RepositoryUploadChunk { + if value == nil { + return nil + } + return &repositorypb.RepositoryUploadChunk{ChunkIndex: int32(value.ChunkIndex), + SizeBytes: int32(value.SizeBytes), Sha256: value.SHA256, CreatedAt: timestamppb.New(value.CreatedAt)} +} + +func repositoryPackageToPB(value *po.RepositoryPackage) *repositorypb.RepositoryPackage { + return &repositorypb.RepositoryPackage{ + Id: value.ID, RepositoryId: value.RepositoryID, RevisionId: value.RevisionID, + Name: value.Name, Epoch: value.Epoch, Version: value.Version, Release: value.Release, + Architecture: value.Architecture, Nevra: value.NEVRA, FileName: value.FileName, + SizeBytes: value.SizeBytes, Sha256: value.SHA256, Source: value.Source, + SignatureStatus: value.SignatureStatus, SignerKeyId: value.SignerKeyID, + SignatureMessage: value.SignatureMessage, + CreatedAt: timestamppb.New(value.CreatedAt), + } +} + +func repositoryQuarantineToPB(value *po.RepositoryQuarantineItem) *repositorypb.RepositoryQuarantineItem { + return &repositorypb.RepositoryQuarantineItem{ + Id: value.ID, RepositoryId: value.RepositoryID, TaskId: value.TaskID, UploadId: value.UploadID, + OriginalName: value.OriginalName, SizeBytes: value.SizeBytes, Sha256: value.SHA256, + ReasonCode: value.ReasonCode, ReasonMessage: value.ReasonMessage, DetectedNevra: value.DetectedNEVRA, + CreatedAt: timestamppb.New(value.CreatedAt), + } +} diff --git a/manager/backend/services/footstone/presentation/repository_content_controller_test.go b/manager/backend/services/footstone/presentation/repository_content_controller_test.go new file mode 100644 index 0000000000000000000000000000000000000000..19a07f1242596529f8591541280246da6e01d207 --- /dev/null +++ b/manager/backend/services/footstone/presentation/repository_content_controller_test.go @@ -0,0 +1,62 @@ +package presentation + +import ( + "reflect" + "testing" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" +) + +func TestRepositoryConfigToPB(t *testing.T) { + value := &request.GeneratedRepositoryConfig{ + RepositoryID: "repository-id", ActiveRevisionID: "revision-id", RepoID: "ocmanager-publish-key", + FileName: "ocmanager-publish-key.repo", ContentType: "text/plain; charset=utf-8", + Content: "[ocmanager-publish-key]\n", BaseURL: "https://repo.test/current/", + RevisionURL: "https://repo.test/revisions/revision-id/", ImmutableRevision: true, + Enabled: true, GPGCheck: true, RepoGPGCheck: false, + GPGKeyURLs: []string{"https://repo.test/keys/one.key", "https://repo.test/keys/two.key"}, + } + got := repositoryConfigToPB(value) + if got.GetRepositoryId() != value.RepositoryID || got.GetActiveRevisionId() != value.ActiveRevisionID || + got.GetRepoId() != value.RepoID || got.GetContent() != value.Content || got.GetBaseurl() != value.BaseURL || + got.GetRevisionUrl() != value.RevisionURL || !got.GetImmutableRevision() || !got.GetEnabled() || + !got.GetGpgcheck() || got.GetRepoGpgcheck() || !reflect.DeepEqual(got.GetGpgkeyUrls(), value.GPGKeyURLs) { + t.Fatalf("repository config proto=%+v", got) + } + if repositoryConfigToPB(nil) != nil { + t.Fatal("nil repository config must stay nil") + } +} + +func TestRepositoryGPGKeyToPB(t *testing.T) { + createdAt := time.Date(2026, 8, 24, 8, 46, 44, 0, time.UTC) + value := &request.RepositoryGPGKey{ + ID: "key-id", RepositoryID: "repository-id", KeyID: "8B080EF37D8A7D39", + Fingerprint: "322FF9482DD40C28E924932D8B080EF37D8A7D39", + PublicKeyPath: "keys/repository-id/fingerprint/public.key", + PublicKeyURL: "https://repo.test/publish-key/keys/fingerprint/public.key", + CreatedAt: createdAt, + } + got := repositoryGPGKeyToPB(value) + if got.GetId() != value.ID || got.GetRepositoryId() != value.RepositoryID || + got.GetKeyId() != value.KeyID || got.GetFingerprint() != value.Fingerprint || + got.GetPublicKeyPath() != value.PublicKeyPath || got.GetPublicKeyUrl() != value.PublicKeyURL || + !got.GetCreatedAt().AsTime().Equal(createdAt) { + t.Fatalf("repository GPG key proto=%+v", got) + } + if repositoryGPGKeyToPB(nil) != nil { + t.Fatal("nil repository GPG key must stay nil") + } +} + +func TestRepositoryExportToPB(t *testing.T) { + value := repositoryExportToPB(&request.RepositoryExport{ + FileName: "repository-inventory.json", ContentType: "application/json", + SHA256: "abc123", Content: "{}\n", + }) + if value.GetFileName() != "repository-inventory.json" || value.GetContentType() != "application/json" || + value.GetSha256() != "abc123" || value.GetContent() != "{}\n" { + t.Fatalf("repositoryExportToPB() = %#v", value) + } +} diff --git a/manager/backend/services/footstone/presentation/repository_controller.go b/manager/backend/services/footstone/presentation/repository_controller.go new file mode 100644 index 0000000000000000000000000000000000000000..ce3bd6edf3ddbd1409bc03f048b0af644a98f7a8 --- /dev/null +++ b/manager/backend/services/footstone/presentation/repository_controller.go @@ -0,0 +1,218 @@ +package presentation + +import ( + "context" + "fmt" + + commonpb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/common" + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +type RepositoryController struct { + BaseController +} + +func NewRepositoryController(base BaseController) *RepositoryController { + return &RepositoryController{BaseController: base} +} + +func (c *RepositoryController) CreateRepository(ctx context.Context, req *repositorypb.CreateRepositoryRequest) ( + *repositorypb.RepositoryResponse, error, +) { + value, err := c.application.CreateRepository(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + pbValue, err := repositoryToPB(value) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryResponse{Repository: pbValue}, nil +} + +func (c *RepositoryController) UpdateRepository(ctx context.Context, req *repositorypb.UpdateRepositoryRequest) ( + *repositorypb.RepositoryResponse, error, +) { + value, err := c.application.UpdateRepository(c.ToContext(ctx), req) + if err != nil { + return nil, repositoryNodeRPMErrorForAPI(err) + } + pbValue, err := repositoryToPB(value) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryResponse{Repository: pbValue}, nil +} + +func (c *RepositoryController) DescribeRepository(ctx context.Context, req *repositorypb.DescribeRepositoryRequest) ( + *repositorypb.RepositoryResponse, error, +) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + value, err := c.application.DescribeRepository(c.ToContext(ctx), req.GetRepositoryId()) + if err != nil { + return nil, err + } + pbValue, err := repositoryToPB(value) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryResponse{Repository: pbValue}, nil +} + +func (c *RepositoryController) ListRepositories(ctx context.Context, req *repositorypb.ListRepositoriesRequest) ( + *repositorypb.ListRepositoriesResponse, error, +) { + values, page, err := c.application.ListRepositories(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + response := &repositorypb.ListRepositoriesResponse{ + Repositories: make([]*repositorypb.Repository, 0, len(values)), + PageResponse: &commonpb.PageResponse{ + Count: page.Count, + PageSize: int64(page.PageSize), + PageNumber: int64(page.PageNumber), + PageCount: int64(page.PageCount), + }, + } + for index := range values { + pbValue, convertErr := repositoryToPB(&values[index]) + if convertErr != nil { + return nil, convertErr + } + response.Repositories = append(response.Repositories, pbValue) + } + return response, nil +} + +func (c *RepositoryController) DeleteRepository(ctx context.Context, req *repositorypb.DeleteRepositoryRequest) ( + *emptypb.Empty, error, +) { + if err := c.application.DeleteRepository(c.ToContext(ctx), req); err != nil { + return nil, repositoryNodeRPMErrorForAPI(err) + } + return &emptypb.Empty{}, nil +} + +func (c *RepositoryController) BatchApplyRepositories(ctx context.Context, + req *repositorypb.BatchApplyRepositoriesRequest, +) (*repositorypb.BatchApplyRepositoriesResponse, error) { + values, err := c.application.BatchApplyRepositories(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + response := &repositorypb.BatchApplyRepositoriesResponse{ + Results: make([]*repositorypb.BatchApplyRepositoryResult, 0, len(values)), + } + for _, value := range values { + pbValue, convertErr := repositoryToPB(value.Repository) + if convertErr != nil { + return nil, convertErr + } + response.Results = append(response.Results, &repositorypb.BatchApplyRepositoryResult{ + InputIndex: int32(value.InputIndex), + Operation: value.Operation, + Repository: pbValue, + }) + } + return response, nil +} + +func repositoryToPB(value *po.SoftwareRepository) (*repositorypb.Repository, error) { + include, exclude, err := value.PackageFilters() + if err != nil { + return nil, err + } + return &repositorypb.Repository{ + Id: value.ID, + ProjectId: value.ProjectID, + SystemProduct: value.SystemProduct, + ProductVersion: value.ProductVersion, + Name: value.Name, + Description: value.Description, + Architecture: architectureToPB(value.Architecture), + Type: typeToPB(value.Type), + UpstreamUrl: value.UpstreamURL, + StorageLocation: value.StorageLocation, + PublishKey: value.PublishKey, + AllowUpload: value.AllowUpload, + IncludePackages: include, + ExcludePackages: exclude, + GpgPolicy: gpgPolicyToPB(value.GPGPolicy), + Enabled: value.Enabled, + Status: value.Status, + ActiveRevisionId: value.ActiveRevisionID, + Creator: value.Creator, + CreatedAt: timestamppb.New(value.CreatedAt), + UpdatedAt: timestamppb.New(value.UpdatedAt), + PrimaryStorageNodeId: value.PrimaryStorageNodeID, + PublishBaseUrlSnapshot: value.PublishBaseURLSnapshot, + PackageCount: value.PackageCount, + LogicalBytes: value.LogicalBytes, + LastSyncAt: optionalTimestamp(value.LastSyncAt), + LastSyncStatus: taskStatusToPB(value.LastSyncStatus), + SyncMode: syncModeToPB(value.SyncMode), + BandwidthLimitBytes: value.BandwidthLimitBytes, + MaxParallelDownloads: int32(value.MaxParallelDownloads), + MetadataSigningKeyRef: value.MetadataSigningKeyRef, + RevisionCount: value.RevisionCount, + }, nil +} + +func syncModeToPB(value string) repositorypb.RepositorySyncMode { + switch value { + case request.RepositorySyncModeAppend: + return repositorypb.RepositorySyncMode_REPOSITORY_SYNC_MODE_APPEND + case request.RepositorySyncModeMirror: + return repositorypb.RepositorySyncMode_REPOSITORY_SYNC_MODE_MIRROR + default: + return repositorypb.RepositorySyncMode_REPOSITORY_SYNC_MODE_UNSPECIFIED + } +} + +func architectureToPB(value string) repositorypb.RepositoryArchitecture { + switch value { + case request.RepositoryArchitectureX8664: + return repositorypb.RepositoryArchitecture_REPOSITORY_ARCHITECTURE_X86_64 + case request.RepositoryArchitectureARM64: + return repositorypb.RepositoryArchitecture_REPOSITORY_ARCHITECTURE_AARCH64 + case request.RepositoryArchitectureNoarch: + return repositorypb.RepositoryArchitecture_REPOSITORY_ARCHITECTURE_NOARCH + default: + return repositorypb.RepositoryArchitecture_REPOSITORY_ARCHITECTURE_UNSPECIFIED + } +} + +func typeToPB(value string) repositorypb.RepositoryType { + switch value { + case request.RepositoryTypeBaseline: + return repositorypb.RepositoryType_REPOSITORY_TYPE_BASELINE + case request.RepositoryTypeUpdate: + return repositorypb.RepositoryType_REPOSITORY_TYPE_UPDATE + case request.RepositoryTypePrivate: + return repositorypb.RepositoryType_REPOSITORY_TYPE_PRIVATE + default: + return repositorypb.RepositoryType_REPOSITORY_TYPE_UNSPECIFIED + } +} + +func gpgPolicyToPB(value string) repositorypb.RepositoryGPGPolicy { + switch value { + case request.RepositoryGPGDisabled: + return repositorypb.RepositoryGPGPolicy_REPOSITORY_GPG_POLICY_DISABLED + case request.RepositoryGPGVerifyPackage: + return repositorypb.RepositoryGPGPolicy_REPOSITORY_GPG_POLICY_VERIFY_PACKAGE + case request.RepositoryGPGAudit: + return repositorypb.RepositoryGPGPolicy_REPOSITORY_GPG_POLICY_AUDIT + case request.RepositoryGPGEnforce: + return repositorypb.RepositoryGPGPolicy_REPOSITORY_GPG_POLICY_ENFORCE + default: + return repositorypb.RepositoryGPGPolicy_REPOSITORY_GPG_POLICY_UNSPECIFIED + } +} diff --git a/manager/backend/services/footstone/presentation/repository_controller_test.go b/manager/backend/services/footstone/presentation/repository_controller_test.go new file mode 100644 index 0000000000000000000000000000000000000000..aa972d1093e1d358bdeb4d7c6283b8533f73a37c --- /dev/null +++ b/manager/backend/services/footstone/presentation/repository_controller_test.go @@ -0,0 +1,54 @@ +package presentation + +import ( + "testing" + "time" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +func TestRepositoryToPB(t *testing.T) { + now := time.Now().UTC().Truncate(time.Millisecond) + value := &po.SoftwareRepository{ + ID: "11111111-1111-1111-1111-111111111111", + ProjectID: 42, + SystemProduct: "OpenCloudOS", + ProductVersion: "9.6", + Name: "private-apps", + Architecture: request.RepositoryArchitectureX8664, + Type: request.RepositoryTypePrivate, + StorageLocation: "public/publish-key", + PublishKey: "publish-key", + AllowUpload: true, + IncludePackagesJSON: `["ocm-*"]`, + ExcludePackagesJSON: `["*-debug"]`, + GPGPolicy: request.RepositoryGPGVerifyPackage, + Enabled: true, + Status: request.RepositoryStatusEmpty, + RevisionCount: 7, + CreatedAt: now, + UpdatedAt: now, + } + got, err := repositoryToPB(value) + if err != nil { + t.Fatal(err) + } + if got.GetArchitecture() != repositorypb.RepositoryArchitecture_REPOSITORY_ARCHITECTURE_X86_64 || + got.GetType() != repositorypb.RepositoryType_REPOSITORY_TYPE_PRIVATE || + got.GetGpgPolicy() != repositorypb.RepositoryGPGPolicy_REPOSITORY_GPG_POLICY_VERIFY_PACKAGE { + t.Fatalf("repositoryToPB() enum mapping = %s/%s/%s", got.GetArchitecture(), got.GetType(), got.GetGpgPolicy()) + } + if got.GetStorageLocation() != value.StorageLocation || len(got.GetIncludePackages()) != 1 || + len(got.GetExcludePackages()) != 1 || got.GetCreatedAt().AsTime() != now || got.GetRevisionCount() != 7 { + t.Fatalf("repositoryToPB() = %#v", got) + } +} + +func TestRepositoryToPBRejectsInvalidFilterJSON(t *testing.T) { + value := &po.SoftwareRepository{IncludePackagesJSON: "not-json", ExcludePackagesJSON: "[]"} + if _, err := repositoryToPB(value); err == nil { + t.Fatal("repositoryToPB() error = nil") + } +} diff --git a/manager/backend/services/footstone/presentation/repository_distribution_controller.go b/manager/backend/services/footstone/presentation/repository_distribution_controller.go new file mode 100644 index 0000000000000000000000000000000000000000..f8f38675fdd702f0e38141e1b811d792f2527231 --- /dev/null +++ b/manager/backend/services/footstone/presentation/repository_distribution_controller.go @@ -0,0 +1,126 @@ +package presentation + +import ( + "context" + "fmt" + "strconv" + "time" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func (c *RepositoryController) ListRepositoryDistributionNodes(ctx context.Context, + _ *repositorypb.ListRepositoryDistributionNodesRequest, +) (*repositorypb.ListRepositoryDistributionNodesResponse, error) { + values, err := c.application.ListRepositoryDistributionNodes(c.ToContext(ctx)) + if err != nil { + return nil, err + } + response := &repositorypb.ListRepositoryDistributionNodesResponse{ + Nodes: make([]*repositorypb.RepositoryDistributionNode, 0, len(values)), + } + for _, value := range values { + node := &repositorypb.RepositoryDistributionNode{ + AgentId: strconv.FormatUint(value.AgentID, 10), Ip: value.IP, + AssetStatus: value.AssetStatus, Connected: value.Connected, + } + if value.LastHeartbeatAt > 0 { + node.LastHeartbeatAt = timestamppb.New(time.Unix(value.LastHeartbeatAt, 0)) + } + response.Nodes = append(response.Nodes, node) + } + return response, nil +} + +func (c *RepositoryController) CreateRepositoryDistribution(ctx context.Context, + req *repositorypb.CreateRepositoryDistributionRequest, +) (*repositorypb.RepositoryDistributionResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + value, task, err := c.application.CreateRepositoryDistribution(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryDistributionResponse{ + Distribution: repositoryDistributionToPB(value), Task: repositoryTaskToPB(task), + }, nil +} + +func (c *RepositoryController) DescribeRepositoryDistribution(ctx context.Context, + req *repositorypb.DescribeRepositoryDistributionRequest, +) (*repositorypb.RepositoryDistributionResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + value, err := c.application.DescribeRepositoryDistribution(c.ToContext(ctx), req.GetDistributionId()) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryDistributionResponse{Distribution: repositoryDistributionToPB(value)}, nil +} + +func (c *RepositoryController) ListRepositoryDistributions(ctx context.Context, + req *repositorypb.ListRepositoryDistributionsRequest, +) (*repositorypb.ListRepositoryDistributionsResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + values, page, err := c.application.ListRepositoryDistributions(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + response := &repositorypb.ListRepositoryDistributionsResponse{ + Distributions: make([]*repositorypb.RepositoryDistribution, 0, len(values)), + PageResponse: request.ToPageResponse(page), + } + for index := range values { + response.Distributions = append(response.Distributions, repositoryDistributionToPB(&values[index])) + } + return response, nil +} + +func repositoryDistributionToPB(value *po.RepositoryDistribution) *repositorypb.RepositoryDistribution { + if value == nil { + return nil + } + result := &repositorypb.RepositoryDistribution{ + Id: value.ID, RepositoryId: value.RepositoryID, ProjectId: value.ProjectID, + RevisionId: value.RevisionID, RepositoryTaskId: value.RepositoryTaskID, + TmsTaskId: value.TMSTaskID, ArtifactMd5: value.ArtifactMD5, + ManifestSha256: value.ManifestSHA256, RepomdSha256: value.RepomdSHA256, + Status: value.Status, RequestedBy: value.RequestedBy, TimeoutSeconds: int32(value.TimeoutSeconds), + ErrorCode: value.ErrorCode, ErrorMessage: value.ErrorMessage, + CreatedAt: timestamppb.New(value.CreatedAt), UpdatedAt: timestamppb.New(value.UpdatedAt), + Targets: make([]*repositorypb.RepositoryDistributionTarget, 0, len(value.Targets)), + } + if value.StartedAt != nil { + result.StartedAt = timestamppb.New(*value.StartedAt) + } + if value.FinishedAt != nil { + result.FinishedAt = timestamppb.New(*value.FinishedAt) + } + for index := range value.Targets { + result.Targets = append(result.Targets, repositoryDistributionTargetToPB(&value.Targets[index])) + } + return result +} + +func repositoryDistributionTargetToPB(value *po.RepositoryDistributionTarget) *repositorypb.RepositoryDistributionTarget { + result := &repositorypb.RepositoryDistributionTarget{ + Id: value.ID, TargetIp: value.TargetIP, AgentId: strconv.FormatUint(value.AgentID, 10), + Status: value.Status, AttemptCount: int32(value.AttemptCount), ExitCode: value.ExitCode, + Output: value.Output, OutputRef: value.OutputRef, OutputTruncated: value.OutputTruncated, + ErrorCode: value.ErrorCode, ErrorMessage: value.ErrorMessage, + } + if value.StartedAt != nil { + result.StartedAt = timestamppb.New(*value.StartedAt) + } + if value.FinishedAt != nil { + result.FinishedAt = timestamppb.New(*value.FinishedAt) + } + return result +} diff --git a/manager/backend/services/footstone/presentation/repository_distribution_controller_test.go b/manager/backend/services/footstone/presentation/repository_distribution_controller_test.go new file mode 100644 index 0000000000000000000000000000000000000000..9b0d17ba52726ebd2da46e15b6856133338d19a5 --- /dev/null +++ b/manager/backend/services/footstone/presentation/repository_distribution_controller_test.go @@ -0,0 +1,17 @@ +package presentation + +import ( + "testing" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +func TestRepositoryDistributionTargetToPBKeepsUint64AgentIDAsString(t *testing.T) { + value := repositoryDistributionTargetToPB(&po.RepositoryDistributionTarget{ + ID: "target-id", TargetIP: "192.0.2.10", AgentID: 5902982880214862284, + Status: "PARTIAL_SUCCESS", ExitCode: 17, + }) + if value.GetAgentId() != "5902982880214862284" || value.GetExitCode() != 17 { + t.Fatalf("repositoryDistributionTargetToPB() = %#v", value) + } +} diff --git a/manager/backend/services/footstone/presentation/repository_error_catalog.go b/manager/backend/services/footstone/presentation/repository_error_catalog.go new file mode 100644 index 0000000000000000000000000000000000000000..a71bc53706a93cabecdc49e99f9898f11494dd0e --- /dev/null +++ b/manager/backend/services/footstone/presentation/repository_error_catalog.go @@ -0,0 +1,56 @@ +package presentation + +import ( + "errors" + "fmt" + "strings" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorystoragenoderepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryContentService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryErrorCatalog" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryNodeService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryService" +) + +// repositoryErrorLocalization keeps successful responses empty and resolves +// all Repository failures through the single backend-owned catalog. +func repositoryErrorLocalization(code string) (string, string) { + if strings.TrimSpace(code) == "" { + return "", "" + } + entry := repositoryErrorCatalog.Resolve(code) + return entry.ZhMessage, entry.ZhRecommendedAction +} + +// Only known Node RPM domain rejections are localized here; unrelated errors +// keep their original handling and the original cause remains available. +func repositoryNodeRPMErrorForAPI(err error) error { + if err == nil { + return nil + } + code := "" + switch { + case errors.Is(err, repositoryService.ErrSystemManagedRepository): + code = "REPOSITORY_SYSTEM_MANAGED" + case errors.Is(err, repositoryContentService.ErrSystemRepositorySync): + code = "REPOSITORY_NODE_SYNC_FORBIDDEN" + case errors.Is(err, repositoryNodeService.ErrNodeInstallSourceInvalid): + code = "REPOSITORY_NODE_INSTALL_SOURCE_INVALID" + case errors.Is(err, repositorystoragenoderepo.ErrNodeInstallSourceNotFound): + code = "REPOSITORY_NODE_INSTALL_SOURCE_NOT_FOUND" + case errors.Is(err, repositoryNodeService.ErrNodeArtifactInvalid): + code = "REPOSITORY_NODE_ARTIFACT_INVALID" + case errors.Is(err, repositorystoragenoderepo.ErrNodeArtifactNotFound): + code = "REPOSITORY_NODE_ARTIFACT_NOT_FOUND" + case errors.Is(err, repositoryNodeService.ErrNodeRegistrationDenied): + code = "REPOSITORY_NODE_REGISTRATION_DENIED" + default: + return err + } + if strings.HasPrefix(err.Error(), code+":") { + return err + } + entry := repositoryErrorCatalog.Resolve(code) + return fmt.Errorf("%s: %s 建议:%s 技术原因:%w", entry.Code, + entry.ZhMessage, entry.ZhRecommendedAction, err) +} diff --git a/manager/backend/services/footstone/presentation/repository_error_catalog_test.go b/manager/backend/services/footstone/presentation/repository_error_catalog_test.go new file mode 100644 index 0000000000000000000000000000000000000000..305892c75defad2f88168c8d7fa0d32603727d4b --- /dev/null +++ b/manager/backend/services/footstone/presentation/repository_error_catalog_test.go @@ -0,0 +1,110 @@ +package presentation + +import ( + "encoding/json" + "errors" + "fmt" + "net/http/httptest" + "strings" + "testing" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorystoragenoderepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryContentService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryErrorCatalog" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryNodeService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryService" + infraTrpc "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/trpc" + "trpc.group/trpc-go/trpc-go/errs" +) + +func TestRepositoryErrorCatalogPopulatesNodeTargetAndSummaryResponses(t *testing.T) { + now := time.Now().UTC() + node, err := repositoryStorageNodeToPB(&po.RepositoryStorageNode{ + ID: "node-id", LastErrorCode: "REPOSITORY_NODE_OFFLINE", + ToolVersionsJSON: []byte(`{}`), LabelsJSON: []byte(`{}`), CreatedAt: now, UpdatedAt: now, + }) + if err != nil { + t.Fatal(err) + } + if node.GetErrorZhMessage() == "" || node.GetErrorZhRecommendedAction() == "" || + node.GetLastErrorCode() != "REPOSITORY_NODE_OFFLINE" { + t.Fatalf("localized storage node = %#v", node) + } + + target := repositoryTaskTargetToPB(&po.RepositoryTaskTarget{ + ID: "target-id", ErrorCode: "PACKAGE_INSTALL_TARGET_FAILED", ErrorMessage: "dnf failed", + CreatedAt: now, UpdatedAt: now, + }) + if target.GetErrorZhMessage() == "" || target.GetErrorZhRecommendedAction() == "" || + target.GetErrorMessage() != "dnf failed" { + t.Fatalf("localized task target = %#v", target) + } + + summaries := repositoryNodeSummariesToPB([]po.RepositoryNodeRepositorySummary{{ + RepositoryID: "repository-id", LastSyncErrorCode: "SYNC_DOWNLOAD_FAILED", + LastSyncErrorMessage: "connection refused", + }}) + if len(summaries) != 1 || summaries[0].GetLastSyncErrorZhMessage() == "" || + summaries[0].GetLastSyncErrorZhRecommendedAction() == "" || + summaries[0].GetLastSyncErrorMessage() != "connection refused" { + t.Fatalf("localized repository summary = %#v", summaries) + } +} + +func TestRepositoryNodeRPMErrorsUseStableCodesAndCatalogText(t *testing.T) { + for _, test := range []struct { + name string + cause error + code string + }{ + {"system repository", repositoryService.ErrSystemManagedRepository, "REPOSITORY_SYSTEM_MANAGED"}, + {"system sync", repositoryContentService.ErrSystemRepositorySync, "REPOSITORY_NODE_SYNC_FORBIDDEN"}, + {"invalid source", repositoryNodeService.ErrNodeInstallSourceInvalid, "REPOSITORY_NODE_INSTALL_SOURCE_INVALID"}, + {"missing source", repositorystoragenoderepo.ErrNodeInstallSourceNotFound, "REPOSITORY_NODE_INSTALL_SOURCE_NOT_FOUND"}, + {"invalid artifact", repositoryNodeService.ErrNodeArtifactInvalid, "REPOSITORY_NODE_ARTIFACT_INVALID"}, + {"unavailable artifact", repositorystoragenoderepo.ErrNodeArtifactNotFound, "REPOSITORY_NODE_ARTIFACT_NOT_FOUND"}, + {"registration authorization", repositoryNodeService.ErrNodeRegistrationDenied, "REPOSITORY_NODE_REGISTRATION_DENIED"}, + } { + t.Run(test.name, func(t *testing.T) { + original := fmt.Errorf("install validation: %w", test.cause) + localized := repositoryNodeRPMErrorForAPI(original) + if !errors.Is(localized, test.cause) || !strings.Contains(localized.Error(), original.Error()) { + t.Fatalf("original technical cause was lost: %v", localized) + } + if repositoryNodeRPMErrorForAPI(localized) != localized { + t.Fatal("already localized rejection was wrapped twice") + } + recorder := httptest.NewRecorder() + infraTrpc.DefaultErrHandler(recorder, httptest.NewRequest("POST", "/repository-test", nil), + errs.New(999, localized.Error()).(*errs.Error)) + var response infraTrpc.Response + if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil { + t.Fatal(err) + } + entry := repositoryErrorCatalog.Resolve(test.code) + if response.Success || response.ErrorCode != test.code || + !strings.Contains(response.ErrorMsg, entry.ZhMessage) || + !strings.Contains(response.ErrorMsg, entry.ZhRecommendedAction) || + !strings.Contains(response.ErrorMsg, original.Error()) { + t.Fatalf("localized error response = %#v", response) + } + }) + } +} + +func TestRepositoryNodeRPMErrorMappingPreservesSuccessAndOtherErrors(t *testing.T) { + if repositoryNodeRPMErrorForAPI(nil) != nil { + t.Fatal("successful response changed") + } + for _, original := range []error{ + errors.New("unrelated backend failure"), + errors.New("repository node artifact not found"), + errors.New("REVISION_CHANGED: existing encoded failure"), + } { + if got := repositoryNodeRPMErrorForAPI(original); got != original { + t.Fatalf("unrelated error was relabeled: %v", got) + } + } +} diff --git a/manager/backend/services/footstone/presentation/repository_node_bootstrap_controller.go b/manager/backend/services/footstone/presentation/repository_node_bootstrap_controller.go new file mode 100644 index 0000000000000000000000000000000000000000..b96a9630059acf31ab70e33cbd76fff644c3ef9b --- /dev/null +++ b/manager/backend/services/footstone/presentation/repository_node_bootstrap_controller.go @@ -0,0 +1,33 @@ +package presentation + +import ( + "context" + "fmt" + + repositorynodepb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repositorynode" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/application" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" +) + +type RepositoryNodeBootstrapController struct{ application application.Application } + +func NewRepositoryNodeBootstrapController(app application.Application) *RepositoryNodeBootstrapController { + return &RepositoryNodeBootstrapController{application: app} +} + +func (c *RepositoryNodeBootstrapController) BootstrapNode(_ context.Context, + req *repositorynodepb.BootstrapNodeRequest, +) (*repositorynodepb.NodeCertificateResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + bundle, err := c.application.BootstrapRepositoryStorageNode(request.RepositoryNodeBootstrapRequest{ + EnrollmentID: req.GetEnrollmentId(), TaskID: req.GetInstallTaskId(), TMSAgentID: req.GetTmsAgentId(), + AuthorizationID: req.GetAuthorizationId(), Architecture: req.GetArchitecture(), + Token: req.GetBootstrapToken(), CSRPEM: req.GetCsrPem(), + }) + if err != nil { + return nil, err + } + return repositoryNodeCertificateToPB(bundle), nil +} diff --git a/manager/backend/services/footstone/presentation/repository_node_bootstrap_server.go b/manager/backend/services/footstone/presentation/repository_node_bootstrap_server.go new file mode 100644 index 0000000000000000000000000000000000000000..63cbed551ec067a0cca7780a923ab1b142e9db8c --- /dev/null +++ b/manager/backend/services/footstone/presentation/repository_node_bootstrap_server.go @@ -0,0 +1,62 @@ +package presentation + +import ( + "crypto/tls" + "errors" + "fmt" + "net" + "os" + "strings" + "time" + + repositorynodepb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repositorynode" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/application" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "trpc.group/trpc-go/trpc-go/server" +) + +const repositoryNodeBootstrapServiceName = "trpc.ocmanager.footstone.repositorynode.bootstrap" + +func registerRepositoryNodeBootstrapService(s *server.Server, app application.Application, + cfg config.RepositoryNodeBootstrapConfig, +) error { + if !cfg.Enabled { + return nil + } + listener, err := newRepositoryNodeBootstrapListener(cfg) + if err != nil { + return err + } + service := server.New(server.WithServiceName(repositoryNodeBootstrapServiceName), + server.WithAddress(cfg.ListenAddress), server.WithNetwork("tcp"), server.WithProtocol("trpc"), + server.WithTimeout(60*time.Second), server.WithListener(listener)) + s.AddService(repositoryNodeBootstrapServiceName, service) + repositorynodepb.RegisterRepositoryNodeBootstrapServiceService(service, + NewRepositoryNodeBootstrapController(app)) + return nil +} + +func newRepositoryNodeBootstrapListener(cfg config.RepositoryNodeBootstrapConfig) (net.Listener, error) { + cfg.ListenAddress = strings.TrimSpace(cfg.ListenAddress) + if cfg.ListenAddress == "" || cfg.ServerCert == "" || cfg.ServerKey == "" || + cfg.SigningCACert == "" || cfg.SigningCAKey == "" { + return nil, errors.New("repository node bootstrap TLS configuration is incomplete") + } + for label, filePath := range map[string]string{"server certificate": cfg.ServerCert, + "server private key": cfg.ServerKey, "signing CA certificate": cfg.SigningCACert, + "signing CA private key": cfg.SigningCAKey} { + if info, err := os.Stat(filePath); err != nil || !info.Mode().IsRegular() { + return nil, fmt.Errorf("repository node bootstrap %s is unavailable", label) + } + } + certificate, err := tls.LoadX509KeyPair(cfg.ServerCert, cfg.ServerKey) + if err != nil { + return nil, fmt.Errorf("load repository node bootstrap server certificate: %w", err) + } + listener, err := net.Listen("tcp", cfg.ListenAddress) + if err != nil { + return nil, fmt.Errorf("listen for repository node bootstrap RPC: %w", err) + } + return tls.NewListener(listener, &tls.Config{Certificates: []tls.Certificate{certificate}, + MinVersion: tls.VersionTLS12}), nil +} diff --git a/manager/backend/services/footstone/presentation/repository_node_controller.go b/manager/backend/services/footstone/presentation/repository_node_controller.go new file mode 100644 index 0000000000000000000000000000000000000000..507a4328c3377cf87097a601b288bc22af6f3c8b --- /dev/null +++ b/manager/backend/services/footstone/presentation/repository_node_controller.go @@ -0,0 +1,475 @@ +package presentation + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + + commonpb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/common" + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/timestamppb" + thttp "trpc.group/trpc-go/trpc-go/http" +) + +func (c *RepositoryController) CreateRepositoryStorageNode(ctx context.Context, + req *repositorypb.CreateRepositoryStorageNodeRequest, +) (*repositorypb.RepositoryStorageNodeResponse, error) { + value, err := c.application.CreateRepositoryStorageNode(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + pbValue, err := repositoryStorageNodeToPB(value) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryStorageNodeResponse{StorageNode: pbValue}, nil +} + +func (c *RepositoryController) UpdateRepositoryStorageNode(ctx context.Context, + req *repositorypb.UpdateRepositoryStorageNodeRequest, +) (*repositorypb.RepositoryStorageNodeResponse, error) { + value, err := c.application.UpdateRepositoryStorageNode(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + pbValue, err := repositoryStorageNodeToPB(value) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryStorageNodeResponse{StorageNode: pbValue}, nil +} + +func (c *RepositoryController) DescribeRepositoryStorageNode(ctx context.Context, + req *repositorypb.DescribeRepositoryStorageNodeRequest, +) (*repositorypb.RepositoryStorageNodeResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + value, err := c.application.DescribeRepositoryStorageNode(c.ToContext(ctx), req.GetStorageNodeId()) + if err != nil { + return nil, err + } + pbValue, err := repositoryStorageNodeToPB(value) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryStorageNodeResponse{StorageNode: pbValue, + Repositories: repositoryNodeSummariesToPB(value.Repositories), + MetadataSigningKeys: repositoryMetadataSigningKeysToPB(value.MetadataSigningKeys)}, nil +} + +func repositoryMetadataSigningKeysToPB(values []po.RepositoryMetadataSigningKey) []*repositorypb.RepositoryMetadataSigningKey { + result := make([]*repositorypb.RepositoryMetadataSigningKey, 0, len(values)) + for index := range values { + value := &values[index] + result = append(result, &repositorypb.RepositoryMetadataSigningKey{Id: value.ID, + StorageNodeId: value.StorageNodeID, KeyRef: value.KeyRef, Fingerprint: value.Fingerprint, + PublicKeyName: value.PublicKeyName, PublicKeyUrl: value.PublicKeyURL, Status: value.Status, + KeyCreatedAt: optionalTimestamp(value.KeyCreatedAt), ObservedAt: timestamppb.New(value.ObservedAt)}) + } + return result +} + +func (c *RepositoryController) ListRepositoryStorageNodes(ctx context.Context, + req *repositorypb.ListRepositoryStorageNodesRequest, +) (*repositorypb.ListRepositoryStorageNodesResponse, error) { + values, page, statistics, err := c.application.ListRepositoryStorageNodes(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + response := &repositorypb.ListRepositoryStorageNodesResponse{ + StorageNodes: make([]*repositorypb.RepositoryStorageNode, 0, len(values)), + TotalNodes: statistics.TotalNodes, OnlineNodes: statistics.OnlineNodes, + TotalCapacityBytes: statistics.TotalCapacityBytes, UsedCapacityBytes: statistics.UsedCapacityBytes, + CriticalNodes: statistics.CriticalNodes, + PageResponse: &commonpb.PageResponse{Count: page.Count, PageSize: int64(page.PageSize), + PageNumber: int64(page.PageNumber), PageCount: int64(page.PageCount)}, + } + for index := range values { + pbValue, convertErr := repositoryStorageNodeToPB(&values[index]) + if convertErr != nil { + return nil, convertErr + } + response.StorageNodes = append(response.StorageNodes, pbValue) + } + return response, nil +} + +func (c *RepositoryController) DeleteRepositoryStorageNode(ctx context.Context, + req *repositorypb.DeleteRepositoryStorageNodeRequest, +) (*emptypb.Empty, error) { + if err := c.application.DeleteRepositoryStorageNode(c.ToContext(ctx), req); err != nil { + return nil, err + } + return &emptypb.Empty{}, nil +} + +func (c *RepositoryController) ProbeRepositoryStorageNode(ctx context.Context, + req *repositorypb.ProbeRepositoryStorageNodeRequest, +) (*repositorypb.RepositoryStorageNodeProbeResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + node, preflight, err := c.application.ProbeRepositoryStorageNode(c.ToContext(ctx), req.GetStorageNodeId()) + if err != nil { + return nil, err + } + pbNode, err := repositoryStorageNodeToPB(node) + if err != nil { + return nil, err + } + pbPreflight, err := repositoryPreflightToPB(preflight) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryStorageNodeProbeResponse{StorageNode: pbNode, Preflight: pbPreflight}, nil +} + +func (c *RepositoryController) ListRepositoryPreflightRecords(ctx context.Context, + req *repositorypb.ListRepositoryPreflightRecordsRequest, +) (*repositorypb.ListRepositoryPreflightRecordsResponse, error) { + values, page, err := c.application.ListRepositoryPreflightRecords(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + response := &repositorypb.ListRepositoryPreflightRecordsResponse{ + Records: make([]*repositorypb.RepositoryPreflightRecord, 0, len(values)), + PageResponse: &commonpb.PageResponse{Count: page.Count, PageSize: int64(page.PageSize), + PageNumber: int64(page.PageNumber), PageCount: int64(page.PageCount)}, + } + for index := range values { + pbValue, convertErr := repositoryPreflightToPB(&values[index]) + if convertErr != nil { + return nil, convertErr + } + response.Records = append(response.Records, pbValue) + } + return response, nil +} + +func (c *RepositoryController) CreateRepositoryNodeEnrollment(ctx context.Context, + req *repositorypb.CreateRepositoryNodeEnrollmentRequest, +) (*repositorypb.RepositoryNodeEnrollmentResponse, error) { + value, err := c.application.CreateRepositoryNodeEnrollment(c.ToContext(ctx), req) + if err != nil { + return nil, repositoryNodeRPMErrorForAPI(err) + } + node, err := repositoryStorageNodeToPB(value.Node) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryNodeEnrollmentResponse{StorageNode: node, + Enrollment: repositoryNodeEnrollmentToPB(value.Enrollment), Task: repositoryTaskToPB(value.Task), + Target: repositoryTaskTargetToPB(value.Target), BootstrapToken: value.BootstrapToken, + BootstrapExpiresAt: timestamppb.New(value.BootstrapExpires)}, nil +} + +func (c *RepositoryController) RegisterRepositoryNodeArtifact(ctx context.Context, + req *repositorypb.RegisterRepositoryNodeArtifactRequest, +) (*repositorypb.RepositoryNodeArtifactResponse, error) { + value, err := c.application.RegisterRepositoryNodeArtifact(c.ToContext(ctx), req) + if err != nil { + return nil, repositoryNodeRPMErrorForAPI(err) + } + return &repositorypb.RepositoryNodeArtifactResponse{Artifact: repositoryNodeArtifactToPB(value)}, nil +} + +func (c *RepositoryController) ListRepositoryNodeArtifacts(ctx context.Context, + req *repositorypb.ListRepositoryNodeArtifactsRequest, +) (*repositorypb.ListRepositoryNodeArtifactsResponse, error) { + values, err := c.application.ListRepositoryNodeArtifacts(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + response := &repositorypb.ListRepositoryNodeArtifactsResponse{ + Artifacts: make([]*repositorypb.RepositoryNodeArtifact, 0, len(values))} + for index := range values { + response.Artifacts = append(response.Artifacts, repositoryNodeArtifactToPB(&values[index])) + } + return response, nil +} + +func (c *RepositoryController) DownloadRepositoryNodeArtifact(ctx context.Context, + req *repositorypb.DownloadRepositoryNodeArtifactRequest, +) (*emptypb.Empty, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + value, err := c.application.DownloadRepositoryNodeArtifact(c.ToContext(ctx), req.GetArtifactId()) + if err != nil { + return nil, repositoryNodeRPMErrorForAPI(err) + } + defer value.Content.Close() + response := thttp.Head(ctx).Response + response.Header().Set("Content-Type", value.ContentType) + response.Header().Set("Content-Disposition", "attachment; filename="+value.FileName) + response.Header().Set("Content-Length", strconv.FormatInt(value.SizeBytes, 10)) + response.Header().Set("X-Content-SHA256", value.SHA256) + response.Header().Set("Cache-Control", "private, no-store") + if err = copyRepositoryTaskLog(response, value.Content); err != nil { + return nil, fmt.Errorf("stream repository node artifact: %w", err) + } + return &emptypb.Empty{}, nil +} + +func (c *RepositoryController) ConfigureRepositoryNodeInstallSource(ctx context.Context, + req *repositorypb.ConfigureRepositoryNodeInstallSourceRequest, +) (*repositorypb.RepositoryNodeInstallSourceResponse, error) { + value, err := c.application.ConfigureRepositoryNodeInstallSource(c.ToContext(ctx), req) + if err != nil { + return nil, repositoryNodeRPMErrorForAPI(err) + } + return &repositorypb.RepositoryNodeInstallSourceResponse{InstallSource: repositoryNodeInstallSourceToPB(value)}, nil +} + +func (c *RepositoryController) ListRepositoryNodeInstallSources(ctx context.Context, + req *repositorypb.ListRepositoryNodeInstallSourcesRequest, +) (*repositorypb.ListRepositoryNodeInstallSourcesResponse, error) { + values, err := c.application.ListRepositoryNodeInstallSources(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + response := &repositorypb.ListRepositoryNodeInstallSourcesResponse{ + InstallSources: make([]*repositorypb.RepositoryNodeInstallSource, 0, len(values))} + for index := range values { + response.InstallSources = append(response.InstallSources, repositoryNodeInstallSourceToPB(&values[index])) + } + return response, nil +} + +func (c *RepositoryController) UpsertRepositoryNodeRegistrationAllowlist(ctx context.Context, + req *repositorypb.UpsertRepositoryNodeRegistrationAllowlistRequest, +) (*repositorypb.RepositoryNodeRegistrationAllowlistResponse, error) { + value, err := c.application.UpsertRepositoryNodeRegistrationAllowlist(c.ToContext(ctx), req) + if err != nil { + return nil, repositoryNodeRPMErrorForAPI(err) + } + return &repositorypb.RepositoryNodeRegistrationAllowlistResponse{ + Allowlist: repositoryNodeRegistrationAllowlistToPB(value)}, nil +} + +func (c *RepositoryController) ListRepositoryNodeRegistrationAllowlists(ctx context.Context, + req *repositorypb.ListRepositoryNodeRegistrationAllowlistsRequest, +) (*repositorypb.ListRepositoryNodeRegistrationAllowlistsResponse, error) { + values, err := c.application.ListRepositoryNodeRegistrationAllowlists(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + response := &repositorypb.ListRepositoryNodeRegistrationAllowlistsResponse{ + Allowlists: make([]*repositorypb.RepositoryNodeRegistrationAllowlist, 0, len(values))} + for index := range values { + response.Allowlists = append(response.Allowlists, repositoryNodeRegistrationAllowlistToPB(&values[index])) + } + return response, nil +} + +func repositoryNodeEnrollmentToPB(value *po.RepositoryNodeEnrollment) *repositorypb.RepositoryNodeEnrollment { + if value == nil { + return nil + } + return &repositorypb.RepositoryNodeEnrollment{Id: value.ID, StorageNodeId: value.NodeID, + ProjectId: value.ProjectID, TmsAgentId: value.TMSAgentID, Generation: int32(value.Generation), + Status: enrollmentStatusToPB(value.Status), InstallTaskId: value.InstallTaskID, + TmsTaskId: value.TMSTaskID, + ExpectedArchitecture: architectureToPB(value.ExpectedArchitecture), ExpiresAt: timestamppb.New(value.ExpiresAt), + LastErrorCode: value.LastErrorCode, LastErrorMessage: value.LastErrorMessage, + AllowlistEntryId: value.AllowlistEntryID, ArtifactId: value.ArtifactID, + AuthorizationId: repositoryNodeEnrollmentAuthorizationID(value), CsrSha256: value.CSRSHA256, + IssuanceStatus: value.IssuanceStatus, CreatedBy: value.CreatedBy, + CreatedAt: timestamppb.New(value.CreatedAt), UpdatedAt: timestamppb.New(value.UpdatedAt)} +} + +func repositoryNodeEnrollmentAuthorizationID(value *po.RepositoryNodeEnrollment) string { + if value == nil || value.AuthorizationID == nil { + return "" + } + return *value.AuthorizationID +} + +func enrollmentStatusToPB(value string) repositorypb.RepositoryNodeEnrollmentStatus { + switch value { + case request.RepositoryNodeEnrollmentPending: + return repositorypb.RepositoryNodeEnrollmentStatus_REPOSITORY_NODE_ENROLLMENT_STATUS_PENDING + case request.RepositoryNodeEnrollmentInstalling: + return repositorypb.RepositoryNodeEnrollmentStatus_REPOSITORY_NODE_ENROLLMENT_STATUS_INSTALLING + case request.RepositoryNodeEnrollmentBootstrapped: + return repositorypb.RepositoryNodeEnrollmentStatus_REPOSITORY_NODE_ENROLLMENT_STATUS_BOOTSTRAPPED + case request.RepositoryNodeEnrollmentOnline: + return repositorypb.RepositoryNodeEnrollmentStatus_REPOSITORY_NODE_ENROLLMENT_STATUS_ONLINE + case request.RepositoryNodeEnrollmentFailed: + return repositorypb.RepositoryNodeEnrollmentStatus_REPOSITORY_NODE_ENROLLMENT_STATUS_FAILED + case request.RepositoryNodeEnrollmentExpired: + return repositorypb.RepositoryNodeEnrollmentStatus_REPOSITORY_NODE_ENROLLMENT_STATUS_EXPIRED + case request.RepositoryNodeEnrollmentAuthorized: + return repositorypb.RepositoryNodeEnrollmentStatus_REPOSITORY_NODE_ENROLLMENT_STATUS_AUTHORIZED + case request.RepositoryNodeEnrollmentRegistering: + return repositorypb.RepositoryNodeEnrollmentStatus_REPOSITORY_NODE_ENROLLMENT_STATUS_REGISTERING + case request.RepositoryNodeEnrollmentRegistered: + return repositorypb.RepositoryNodeEnrollmentStatus_REPOSITORY_NODE_ENROLLMENT_STATUS_REGISTERED + default: + return repositorypb.RepositoryNodeEnrollmentStatus_REPOSITORY_NODE_ENROLLMENT_STATUS_UNSPECIFIED + } +} + +func repositoryTaskTargetToPB(value *po.RepositoryTaskTarget) *repositorypb.RepositoryTaskTarget { + if value == nil { + return nil + } + errorMessage, recommendedAction := repositoryErrorLocalization(value.ErrorCode) + return &repositorypb.RepositoryTaskTarget{Id: value.ID, TaskId: value.TaskID, + TargetType: value.TargetType, TargetId: value.TargetID, StorageNodeId: value.StorageNodeID, + TmsAgentId: value.TMSAgentID, Status: taskStatusToPB(value.Status), ProgressPercent: value.ProgressPercent, + StartedAt: optionalTimestamp(value.StartedAt), FinishedAt: optionalTimestamp(value.FinishedAt), + ErrorCode: value.ErrorCode, ErrorMessage: value.ErrorMessage, + ErrorZhMessage: errorMessage, ErrorZhRecommendedAction: recommendedAction, + ResultJson: string(value.ResultJSON), + CreatedAt: timestamppb.New(value.CreatedAt), UpdatedAt: timestamppb.New(value.UpdatedAt)} +} + +func repositoryNodeArtifactToPB(value *po.RepositoryNodeArtifact) *repositorypb.RepositoryNodeArtifact { + if value == nil { + return nil + } + return &repositorypb.RepositoryNodeArtifact{Id: value.ID, Name: value.Name, Version: value.Version, + Release: value.Release, Architecture: architectureToPB(value.Architecture), FileName: value.FileName, + SizeBytes: value.SizeBytes, Sha256: value.SHA256, BuildCommit: value.BuildCommit, + RelativePath: value.RelativePath, Status: value.Status, CreatedAt: timestamppb.New(value.CreatedAt), + ProjectId: value.ProjectID, SourceKind: value.SourceKind, SourceRepositoryId: value.SourceRepositoryID, + RevisionId: value.RevisionID, PackageId: value.PackageID, Epoch: value.Epoch, + Nevra: value.NEVRA, ImmutableUrl: value.ImmutableURL} +} + +func repositoryNodeInstallSourceToPB(value *po.RepositoryNodeInstallSource) *repositorypb.RepositoryNodeInstallSource { + if value == nil { + return nil + } + return &repositorypb.RepositoryNodeInstallSource{Id: value.ID, ProjectId: value.ProjectID, + RepositoryId: value.RepositoryID, Architecture: architectureToPB(value.Architecture), Status: value.Status, + CreatedBy: value.CreatedBy, UpdatedBy: value.UpdatedBy, + CreatedAt: timestamppb.New(value.CreatedAt), UpdatedAt: timestamppb.New(value.UpdatedAt)} +} + +func repositoryNodeRegistrationAllowlistToPB(value *po.RepositoryNodeRegistrationAllowlist) *repositorypb.RepositoryNodeRegistrationAllowlist { + if value == nil { + return nil + } + return &repositorypb.RepositoryNodeRegistrationAllowlist{Id: value.ID, ProjectId: value.ProjectID, + TmsAgentId: value.TMSAgentID, ExpectedHostname: value.ExpectedHostname, ExpectedIp: value.ExpectedIP, + ExpectedArchitecture: architectureToPB(value.ExpectedArchitecture), Status: value.Status, + ExpiresAt: optionalTimestamp(value.ExpiresAt), CreatedBy: value.CreatedBy, UpdatedBy: value.UpdatedBy, + CreatedAt: timestamppb.New(value.CreatedAt), UpdatedAt: timestamppb.New(value.UpdatedAt)} +} + +func repositoryStorageNodeToPB(value *po.RepositoryStorageNode) (*repositorypb.RepositoryStorageNode, error) { + if value == nil { + return nil, nil + } + tools, err := decodeStringMap(value.ToolVersionsJSON) + if err != nil { + return nil, fmt.Errorf("decode repository storage node tool versions: %w", err) + } + labels, err := decodeStringMap(value.LabelsJSON) + if err != nil { + return nil, fmt.Errorf("decode repository storage node labels: %w", err) + } + usedBytes := value.CapacityBytes - value.FreeBytes + if usedBytes < 0 { + usedBytes = 0 + } + usagePercent := float64(0) + if value.CapacityBytes > 0 { + usagePercent = float64(usedBytes) * 100 / float64(value.CapacityBytes) + } + errorMessage, recommendedAction := repositoryErrorLocalization(value.LastErrorCode) + return &repositorypb.RepositoryStorageNode{ + Id: value.ID, ProjectId: value.ProjectID, Name: value.Name, Description: value.Description, + ControlBaseUrl: value.ControlBaseURL, PublishBaseUrl: value.PublishBaseURL, + TlsServerName: value.TLSServerName, CertificateFingerprint: value.CertificateFingerprint, + Status: storageNodeStatusToPB(value.Status), Enabled: value.Enabled, + FilesystemType: value.FilesystemType, CapacityBytes: value.CapacityBytes, + FreeBytes: value.FreeBytes, FreeInodes: value.FreeInodes, ToolVersions: tools, Labels: labels, + LastHeartbeatAt: optionalTimestamp(value.LastHeartbeatAt), LastProbeAt: optionalTimestamp(value.LastProbeAt), + LastErrorCode: value.LastErrorCode, LastErrorMessage: value.LastErrorMessage, + Hostname: value.Hostname, IpAddress: value.IPAddress, Architecture: value.Architecture, + OsName: value.OSName, OsVersion: value.OSVersion, AgentVersion: value.AgentVersion, + DiskUsedBytes: usedBytes, DiskUsagePercent: usagePercent, + RepositoryCount: value.RepositoryCount, RepositoryPhysicalBytes: value.RepositoryPhysicalBytes, + ErrorZhMessage: errorMessage, ErrorZhRecommendedAction: recommendedAction, + CreatedBy: value.CreatedBy, CreatedAt: timestamppb.New(value.CreatedAt), UpdatedAt: timestamppb.New(value.UpdatedAt), + }, nil +} + +func repositoryNodeSummariesToPB(values []po.RepositoryNodeRepositorySummary) []*repositorypb.RepositoryNodeRepositorySummary { + result := make([]*repositorypb.RepositoryNodeRepositorySummary, 0, len(values)) + for index := range values { + value := &values[index] + errorMessage, recommendedAction := repositoryErrorLocalization(value.LastSyncErrorCode) + result = append(result, &repositorypb.RepositoryNodeRepositorySummary{ + RepositoryId: value.RepositoryID, Name: value.Name, SystemProduct: value.SystemProduct, + ProductVersion: value.ProductVersion, RepositoryType: typeToPB(value.RepositoryType), + Architecture: architectureToPB(value.Architecture), UpstreamUrl: value.UpstreamURL, + PackageCount: value.PackageCount, LogicalBytes: value.LogicalBytes, PhysicalBytes: value.PhysicalBytes, + CurrentRevision: value.CurrentRevision, PublishStatus: value.PublishStatus, + LastSyncAt: optionalTimestamp(value.LastSyncAt), LastSyncStatus: taskStatusToPB(value.LastSyncStatus), + NextSyncAt: optionalTimestamp(value.NextSyncAt), RunningTaskId: value.RunningTaskID, + LastSyncErrorCode: value.LastSyncErrorCode, LastSyncErrorMessage: value.LastSyncErrorMessage, + LastSyncErrorZhMessage: errorMessage, LastSyncErrorZhRecommendedAction: recommendedAction, + }) + } + return result +} + +func repositoryPreflightToPB(value *po.RepositoryPreflightRecord) (*repositorypb.RepositoryPreflightRecord, error) { + if value == nil { + return nil, nil + } + tools, err := decodeStringMap(value.ToolVersionsJSON) + if err != nil { + return nil, fmt.Errorf("decode repository preflight tool versions: %w", err) + } + return &repositorypb.RepositoryPreflightRecord{ + Id: value.ID, StorageNodeId: value.StorageNodeID, RepositoryId: value.RepositoryID, TaskId: value.TaskID, + Status: value.Status, FilesystemType: value.FilesystemType, CapacityBytes: value.CapacityBytes, + FreeBytes: value.FreeBytes, RequiredFreeBytes: value.RequiredFreeBytes, + FreeInodes: value.FreeInodes, RequiredInodes: value.RequiredInodes, + EstimatedPackageCount: value.EstimatedPackageCount, EstimatedImportBytes: value.EstimatedImportBytes, + ToolVersions: tools, ErrorCode: value.ErrorCode, ErrorMessage: value.ErrorMessage, + ExpiresAt: timestamppb.New(value.ExpiresAt), CreatedAt: timestamppb.New(value.CreatedAt), + }, nil +} + +func storageNodeStatusToPB(value string) repositorypb.RepositoryStorageNodeStatus { + switch value { + case request.RepositoryStorageNodeStatusUnknown: + return repositorypb.RepositoryStorageNodeStatus_REPOSITORY_STORAGE_NODE_STATUS_UNKNOWN + case request.RepositoryStorageNodeStatusOnline: + return repositorypb.RepositoryStorageNodeStatus_REPOSITORY_STORAGE_NODE_STATUS_ONLINE + case request.RepositoryStorageNodeStatusOffline: + return repositorypb.RepositoryStorageNodeStatus_REPOSITORY_STORAGE_NODE_STATUS_OFFLINE + case request.RepositoryStorageNodeStatusDisabled: + return repositorypb.RepositoryStorageNodeStatus_REPOSITORY_STORAGE_NODE_STATUS_DISABLED + case request.RepositoryStorageNodeStatusPending: + return repositorypb.RepositoryStorageNodeStatus_REPOSITORY_STORAGE_NODE_STATUS_PENDING + case request.RepositoryStorageNodeStatusInstalling: + return repositorypb.RepositoryStorageNodeStatus_REPOSITORY_STORAGE_NODE_STATUS_INSTALLING + case request.RepositoryStorageNodeStatusInstallFailed: + return repositorypb.RepositoryStorageNodeStatus_REPOSITORY_STORAGE_NODE_STATUS_INSTALL_FAILED + default: + return repositorypb.RepositoryStorageNodeStatus_REPOSITORY_STORAGE_NODE_STATUS_UNSPECIFIED + } +} + +func decodeStringMap(raw []byte) (map[string]string, error) { + result := map[string]string{} + if len(raw) == 0 { + return result, nil + } + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return result, nil +} diff --git a/manager/backend/services/footstone/presentation/repository_node_cross_host_integration_test.go b/manager/backend/services/footstone/presentation/repository_node_cross_host_integration_test.go new file mode 100644 index 0000000000000000000000000000000000000000..f8dc6b6900a440e6cbcc667917856240ef0d450a --- /dev/null +++ b/manager/backend/services/footstone/presentation/repository_node_cross_host_integration_test.go @@ -0,0 +1,234 @@ +//go:build crossnode + +package presentation + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "strconv" + "strings" + "sync" + "testing" + "time" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + repositorynodepb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repositorynode" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/timestamppb" + "trpc.group/trpc-go/trpc-go/server" +) + +const ( + crossHostNodeID = "a1111111-1111-4111-8111-111111111111" + crossHostTaskID = "a2222222-2222-4222-8222-222222222222" + crossHostRepositoryID = "a3333333-3333-4333-8333-333333333333" + crossHostUploadID = "a4444444-4444-4444-8444-444444444444" + crossHostPublishKey = "cross-node-upload" +) + +func TestRepositoryWorkerCrossHostUploadLifecycle(t *testing.T) { + serverCert := os.Getenv("OCM_CROSS_NODE_SERVER_CERT") + serverKey := os.Getenv("OCM_CROSS_NODE_SERVER_KEY") + clientCA := os.Getenv("OCM_CROSS_NODE_CLIENT_CA") + expectedFingerprint := strings.ToLower(os.Getenv("OCM_CROSS_NODE_CLIENT_FINGERPRINT")) + uploadSHA := strings.ToLower(os.Getenv("OCM_CROSS_NODE_UPLOAD_SHA256")) + uploadSize, sizeErr := strconv.ParseInt(os.Getenv("OCM_CROSS_NODE_UPLOAD_SIZE"), 10, 64) + if serverCert == "" || serverKey == "" || clientCA == "" || len(expectedFingerprint) != 64 || + len(uploadSHA) != 64 || sizeErr != nil || uploadSize <= 0 { + t.Skip("cross-host repository worker environment is not configured") + } + payload, err := json.Marshal(request.RepositoryUploadSnapshot{ + UploadID: crossHostUploadID, FileName: "ocm-cross-node.rpm", Kind: request.RepositoryUploadKindRPM, + RelativePath: "staging/uploads/" + crossHostUploadID + "/content", + SizeBytes: uploadSize, SHA256: uploadSHA, + }) + if err != nil { + t.Fatal(err) + } + fixture := &crossHostControlFixture{expectedFingerprint: expectedFingerprint, + task: &repositorynodepb.RepositoryTaskSpec{ + TaskId: crossHostTaskID, RepositoryId: crossHostRepositoryID, StorageNodeId: crossHostNodeID, + TaskType: repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_UPLOAD, + FencingToken: 1, LeaseExpiresAt: timestamppb.New(time.Now().Add(2 * time.Minute)), + PayloadJson: string(payload), PublishKey: crossHostPublishKey, + Architecture: request.RepositoryArchitectureX8664, GpgPolicy: request.RepositoryGPGDisabled, + }, done: make(chan crossHostControlResult, 1)} + + listener, err := newRepositoryNodeIdentityListener(config.RepositoryInternalControlConfig{ + ListenAddress: envOrDefault("OCM_CROSS_NODE_LISTEN_ADDRESS", "0.0.0.0:19443"), + ServerCert: serverCert, ServerKey: serverKey, ClientCA: clientCA, + }) + if err != nil { + t.Fatal(err) + } + service := server.New(server.WithServiceName(repositoryNodeInternalServiceName), + server.WithAddress(listener.Addr().String()), server.WithNetwork("tcp"), server.WithProtocol("trpc"), + server.WithTimeout(60*time.Second), server.WithListener(listener)) + repositorynodepb.RegisterRepositoryNodeInternalServiceService(service, fixture) + serveErrors := make(chan error, 1) + go func() { serveErrors <- service.Serve() }() + defer service.Close(nil) + t.Logf("CROSS_NODE_CONTROL_READY address=%s node=%s task=%s", listener.Addr(), crossHostNodeID, + crossHostTaskID) + + timer := time.NewTimer(3 * time.Minute) + defer timer.Stop() + select { + case result := <-fixture.done: + if result.err != nil { + t.Fatal(result.err) + } + t.Logf("cross_node_remote_worker=PASS node=%s task=%s revision=%s manifest_sha256=%s "+ + "result_object=%s result_sha256=%s package_count=%d total_bytes=%d", + crossHostNodeID, crossHostTaskID, result.revisionID, result.manifestSHA256, + result.resultObjectID, result.resultObjectSHA256, result.packageCount, result.totalBytes) + case err = <-serveErrors: + t.Fatalf("cross-host repository control service stopped: %v", err) + case <-timer.C: + t.Fatal("timed out waiting for the cross-host repository worker") + } +} + +func envOrDefault(name, fallback string) string { + if value := strings.TrimSpace(os.Getenv(name)); value != "" { + return value + } + return fallback +} + +type crossHostControlResult struct { + revisionID, manifestSHA256, resultObjectID, resultObjectSHA256 string + packageCount, totalBytes int64 + err error +} + +type crossHostControlFixture struct { + repositorynodepb.UnimplementedRepositoryNodeInternalService + mu sync.Mutex + expectedFingerprint string + task *repositorynodepb.RepositoryTaskSpec + claimed bool + prepared crossHostControlResult + commitToken string + done chan crossHostControlResult +} + +func (f *crossHostControlFixture) authenticate(ctx context.Context, nodeID string) error { + if nodeID != crossHostNodeID { + return fmt.Errorf("cross-host request used unexpected node %q", nodeID) + } + fingerprint, ok := repositoryNodeCertificateFingerprint(ctx) + if !ok || fingerprint != f.expectedFingerprint { + return errors.New("cross-host request did not carry the expected verified mTLS identity") + } + return nil +} + +func (f *crossHostControlFixture) HeartbeatNode(ctx context.Context, + input *repositorynodepb.HeartbeatNodeRequest, +) (*repositorynodepb.HeartbeatNodeResponse, error) { + if err := f.authenticate(ctx, input.GetStorageNodeId()); err != nil { + return nil, err + } + if input.GetCapacityBytes() <= 0 || input.GetFreeBytes() <= 0 || input.GetFilesystemType() == "" { + return nil, errors.New("repository node heartbeat did not include usable capacity data") + } + return &repositorynodepb.HeartbeatNodeResponse{ServerTime: timestamppb.Now(), + NextHeartbeatSeconds: 10, NodeEnabled: true}, nil +} + +func (f *crossHostControlFixture) ClaimTask(ctx context.Context, + input *repositorynodepb.ClaimTaskRequest, +) (*repositorynodepb.ClaimTaskResponse, error) { + if err := f.authenticate(ctx, input.GetStorageNodeId()); err != nil { + return nil, err + } + if strings.TrimSpace(input.GetWorkerId()) == "" || input.GetLeaseSeconds() < 5 { + return nil, errors.New("repository worker claim identity or lease is invalid") + } + f.mu.Lock() + defer f.mu.Unlock() + if f.claimed { + return &repositorynodepb.ClaimTaskResponse{}, nil + } + f.claimed = true + return &repositorynodepb.ClaimTaskResponse{Task: f.task}, nil +} + +func (f *crossHostControlFixture) HeartbeatTask(ctx context.Context, + input *repositorynodepb.HeartbeatTaskRequest, +) (*repositorynodepb.HeartbeatTaskResponse, error) { + if err := f.authenticate(ctx, input.GetStorageNodeId()); err != nil { + return nil, err + } + if input.GetTaskId() != crossHostTaskID || input.GetFencingToken() != 1 { + return nil, errors.New("repository task heartbeat identity is invalid") + } + return &repositorynodepb.HeartbeatTaskResponse{LeaseExpiresAt: timestamppb.New(time.Now().Add(time.Minute))}, nil +} + +func (f *crossHostControlFixture) ReportTaskFailure(ctx context.Context, + input *repositorynodepb.ReportTaskFailureRequest, +) (*emptypb.Empty, error) { + if err := f.authenticate(ctx, input.GetStorageNodeId()); err != nil { + return nil, err + } + f.finish(crossHostControlResult{err: fmt.Errorf("remote repository task failed: code=%s message=%s", + input.GetErrorCode(), input.GetErrorMessage())}) + return &emptypb.Empty{}, nil +} + +func (f *crossHostControlFixture) PrepareTaskCommit(ctx context.Context, + input *repositorynodepb.PrepareTaskCommitRequest, +) (*repositorynodepb.PrepareTaskCommitResponse, error) { + if err := f.authenticate(ctx, input.GetStorageNodeId()); err != nil { + return nil, err + } + if input.GetTaskId() != crossHostTaskID || input.GetFencingToken() != 1 || + input.GetCompletionStatus() != repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_SUCCESS || + input.GetRevisionId() == "" || len(input.GetManifestSha256()) != 64 || + input.GetResultObjectId() == "" || len(input.GetResultObjectSha256()) != 64 || + input.GetPackageCount() != 1 || input.GetTotalBytes() <= 0 { + return nil, errors.New("remote repository prepare result is incomplete or inconsistent") + } + f.mu.Lock() + f.prepared = crossHostControlResult{revisionID: input.GetRevisionId(), + manifestSHA256: input.GetManifestSha256(), resultObjectID: input.GetResultObjectId(), + resultObjectSHA256: input.GetResultObjectSha256(), packageCount: input.GetPackageCount(), + totalBytes: input.GetTotalBytes()} + f.commitToken = strings.Repeat("c", 64) + commitToken := f.commitToken + f.mu.Unlock() + return &repositorynodepb.PrepareTaskCommitResponse{CommitToken: commitToken, + RevisionId: input.GetRevisionId(), ManifestSha256: input.GetManifestSha256(), State: "PREPARED"}, nil +} + +func (f *crossHostControlFixture) ConfirmTaskCommit(ctx context.Context, + input *repositorynodepb.ConfirmTaskCommitRequest, +) (*emptypb.Empty, error) { + if err := f.authenticate(ctx, input.GetStorageNodeId()); err != nil { + return nil, err + } + f.mu.Lock() + result := f.prepared + valid := input.GetTaskId() == crossHostTaskID && input.GetFencingToken() == 1 && + f.commitToken != "" && input.GetCommitToken() == f.commitToken && result.revisionID != "" + f.mu.Unlock() + if !valid { + return nil, errors.New("remote repository confirm identity is invalid") + } + f.finish(result) + return &emptypb.Empty{}, nil +} + +func (f *crossHostControlFixture) finish(result crossHostControlResult) { + select { + case f.done <- result: + default: + } +} diff --git a/manager/backend/services/footstone/presentation/repository_node_internal_controller.go b/manager/backend/services/footstone/presentation/repository_node_internal_controller.go new file mode 100644 index 0000000000000000000000000000000000000000..0a16b5c535665bb232bd01e7d1e525b4262344f0 --- /dev/null +++ b/manager/backend/services/footstone/presentation/repository_node_internal_controller.go @@ -0,0 +1,321 @@ +package presentation + +import ( + "context" + "fmt" + "time" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + repositorynodepb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repositorynode" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/application" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +type RepositoryNodeInternalController struct{ application application.Application } + +func NewRepositoryNodeInternalController(app application.Application) *RepositoryNodeInternalController { + return &RepositoryNodeInternalController{application: app} +} + +func (c *RepositoryNodeInternalController) HeartbeatNode(ctx context.Context, + req *repositorynodepb.HeartbeatNodeRequest, +) (*repositorynodepb.HeartbeatNodeResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + if err := c.authenticateNode(ctx, req.GetStorageNodeId()); err != nil { + return nil, err + } + observedAt := time.Time{} + if req.GetObservedAt() != nil && req.GetObservedAt().IsValid() { + observedAt = req.GetObservedAt().AsTime() + } + node, err := c.application.HeartbeatRepositoryStorageNode(request.RepositoryStorageNodeHeartbeat{ + NodeID: req.GetStorageNodeId(), Hostname: req.GetHostname(), IPAddress: req.GetIpAddress(), + Architecture: req.GetArchitecture(), OSName: req.GetOsName(), OSVersion: req.GetOsVersion(), + AgentVersion: req.GetAgentVersion(), FilesystemType: req.GetFilesystemType(), + CapacityBytes: req.GetCapacityBytes(), FreeBytes: req.GetFreeBytes(), FreeInodes: req.GetFreeInodes(), + ToolVersions: req.GetToolVersions(), ObservedAt: observedAt, + ErrorCode: req.GetErrorCode(), ErrorMessage: req.GetErrorMessage(), + RepositoryUsages: repositoryUsagesFromPB(req.GetRepositoryUsages()), + RepositoryUsageSnapshot: req.GetRepositoryUsageSnapshot(), + MetadataSigningKeys: metadataSigningKeysFromPB(req.GetMetadataSigningKeys()), + MetadataKeySnapshot: req.GetMetadataSigningKeySnapshot(), + }) + if err != nil { + return nil, err + } + return &repositorynodepb.HeartbeatNodeResponse{ + ServerTime: timestamppb.Now(), NextHeartbeatSeconds: 10, NodeEnabled: node.Enabled, + }, nil +} + +func metadataSigningKeysFromPB(values []*repositorynodepb.MetadataSigningKeyInventory) []request.RepositoryMetadataSigningKey { + result := make([]request.RepositoryMetadataSigningKey, 0, len(values)) + for _, value := range values { + if value == nil { + continue + } + observedAt := time.Time{} + if value.GetObservedAt() != nil && value.GetObservedAt().IsValid() { + observedAt = value.GetObservedAt().AsTime() + } + var keyCreatedAt *time.Time + if value.GetKeyCreatedAt() != nil && value.GetKeyCreatedAt().IsValid() { + created := value.GetKeyCreatedAt().AsTime() + keyCreatedAt = &created + } + result = append(result, request.RepositoryMetadataSigningKey{KeyRef: value.GetKeyRef(), + Fingerprint: value.GetFingerprint(), PublicKeyName: value.GetPublicKeyName(), + PublicKeyURL: value.GetPublicKeyUrl(), Status: value.GetStatus(), KeyCreatedAt: keyCreatedAt, + ObservedAt: observedAt}) + } + return result +} + +func repositoryUsagesFromPB(values []*repositorynodepb.RepositoryUsage) []request.RepositoryNodeRepositoryUsage { + result := make([]request.RepositoryNodeRepositoryUsage, 0, len(values)) + for _, value := range values { + if value == nil { + continue + } + observedAt := time.Time{} + if value.GetObservedAt() != nil && value.GetObservedAt().IsValid() { + observedAt = value.GetObservedAt().AsTime() + } + result = append(result, request.RepositoryNodeRepositoryUsage{RepositoryID: value.GetRepositoryId(), + LogicalBytes: value.GetLogicalBytes(), PhysicalBytes: value.GetPhysicalBytes(), + PublishStatus: value.GetPublishStatus(), ObservedAt: observedAt}) + } + return result +} + +func (c *RepositoryNodeInternalController) ClaimTask(ctx context.Context, + req *repositorynodepb.ClaimTaskRequest, +) (*repositorynodepb.ClaimTaskResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + if err := c.authenticateNode(ctx, req.GetStorageNodeId()); err != nil { + return nil, err + } + task, repository, err := c.application.ClaimRepositoryTaskForNode( + req.GetStorageNodeId(), req.GetWorkerId(), time.Duration(req.GetLeaseSeconds())*time.Second) + if err != nil { + return nil, err + } + if task == nil { + return &repositorynodepb.ClaimTaskResponse{}, nil + } + var gpgKeys []po.RepositoryGPGKey + if request.RepositoryGPGPolicyEnabled(repository.GPGPolicy) { + gpgKeys, err = c.application.RepositoryGPGKeyMetadataForNode(repository.ID) + if err != nil { + return nil, err + } + } + keyReferences := repositoryGPGKeyReferences(gpgKeys) + metadataKey, err := c.application.RepositoryMetadataSigningKeyForNode(repository) + if err != nil { + return nil, err + } + var metadataReference *repositorynodepb.RepositoryMetadataSigningReference + if metadataKey != nil { + metadataReference = &repositorynodepb.RepositoryMetadataSigningReference{KeyRef: metadataKey.KeyRef, + Fingerprint: metadataKey.Fingerprint, PublicKeyName: metadataKey.PublicKeyName, + PublicKeyUrl: metadataKey.PublicKeyURL} + } + return &repositorynodepb.ClaimTaskResponse{Task: &repositorynodepb.RepositoryTaskSpec{ + TaskId: task.ID, RepositoryId: task.RepositoryID, StorageNodeId: task.StorageNodeID, + PreflightId: task.PreflightID, RevisionId: task.RevisionID, TaskType: taskTypeToPB(task.Type), + FencingToken: task.FencingToken, LeaseExpiresAt: optionalTimestamp(task.LeaseExpiresAt), + PayloadJson: task.PayloadJSON, PublishKey: repository.PublishKey, + Architecture: repository.Architecture, GpgPolicy: repository.GPGPolicy, + ActiveRevisionId: repository.ActiveRevisionID, + GpgKeys: keyReferences, + MetadataSigning: metadataReference, + PublishBaseUrl: repository.PublishBaseURLSnapshot, + }}, nil +} + +func repositoryGPGKeyReferences(keys []po.RepositoryGPGKey) []*repositorynodepb.RepositoryGPGKeyReference { + result := make([]*repositorynodepb.RepositoryGPGKeyReference, 0, len(keys)) + for _, key := range keys { + result = append(result, &repositorynodepb.RepositoryGPGKeyReference{ + KeyId: key.KeyID, Fingerprint: key.Fingerprint, PublicKeyPath: key.PublicKeyPath, + }) + } + return result +} + +func (c *RepositoryNodeInternalController) HeartbeatTask(ctx context.Context, + req *repositorynodepb.HeartbeatTaskRequest, +) (*repositorynodepb.HeartbeatTaskResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + if err := c.authenticateNode(ctx, req.GetStorageNodeId()); err != nil { + return nil, err + } + task, err := c.application.HeartbeatRepositoryNodeTask(request.HeartbeatRepositoryTaskRequest{ + TaskID: req.GetTaskId(), StorageNodeID: req.GetStorageNodeId(), + WorkerID: req.GetWorkerId(), FencingToken: req.GetFencingToken(), + LeaseSeconds: int(req.GetLeaseSeconds()), ProgressPercent: req.GetProgressPercent(), + CurrentStep: req.GetCurrentStep(), + }) + if err != nil { + return nil, err + } + return &repositorynodepb.HeartbeatTaskResponse{ + LeaseExpiresAt: optionalTimestamp(task.LeaseExpiresAt), + CancelRequested: task.Status == request.RepositoryTaskStatusCancelRequested, + }, nil +} + +func (c *RepositoryNodeInternalController) ReportTaskFailure(ctx context.Context, + req *repositorynodepb.ReportTaskFailureRequest, +) (*emptypb.Empty, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + if err := c.authenticateNode(ctx, req.GetStorageNodeId()); err != nil { + return nil, err + } + status := "" + switch req.GetStatus() { + case repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_FAILED: + status = request.RepositoryTaskStatusFailed + case repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_CANCELED: + status = request.RepositoryTaskStatusCanceled + default: + return nil, fmt.Errorf("failure status must be FAILED or CANCELED") + } + _, err := c.application.ReportRepositoryNodeTaskFailure(ctx, request.CompleteRepositoryTaskRequest{ + TaskID: req.GetTaskId(), StorageNodeID: req.GetStorageNodeId(), WorkerID: req.GetWorkerId(), + FencingToken: req.GetFencingToken(), Status: status, + ErrorCode: req.GetErrorCode(), ErrorMessage: req.GetErrorMessage(), + ResultObjectID: req.GetResultObjectId(), ResultObjectSHA256: req.GetResultObjectSha256(), + QuarantineCount: req.GetQuarantineCount(), + }) + if err != nil { + return nil, err + } + return &emptypb.Empty{}, nil +} + +func (c *RepositoryNodeInternalController) ExecuteDistributionTask(ctx context.Context, + req *repositorynodepb.ExecuteDistributionTaskRequest, +) (*repositorynodepb.ExecuteDistributionTaskResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + if err := c.authenticateNode(ctx, req.GetStorageNodeId()); err != nil { + return nil, err + } + task, distribution, err := c.application.ExecuteRepositoryDistributionForNode(ctx, + request.ExecuteRepositoryDistributionTaskRequest{ + TaskID: req.GetTaskId(), StorageNodeID: req.GetStorageNodeId(), + WorkerID: req.GetWorkerId(), FencingToken: req.GetFencingToken(), + }) + if err != nil { + return nil, err + } + return &repositorynodepb.ExecuteDistributionTaskResponse{ + DistributionId: distribution.ID, TmsTaskId: distribution.TMSTaskID, + Status: taskStatusToPB(task.Status), ErrorCode: task.ErrorCode, ErrorMessage: task.ErrorMessage, + }, nil +} + +func (c *RepositoryNodeInternalController) PrepareTaskCommit(ctx context.Context, + req *repositorynodepb.PrepareTaskCommitRequest, +) (*repositorynodepb.PrepareTaskCommitResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + if err := c.authenticateNode(ctx, req.GetStorageNodeId()); err != nil { + return nil, err + } + task, err := c.application.PrepareRepositoryNodeTaskCommit(ctx, request.PrepareRepositoryTaskCommitRequest{ + TaskID: req.GetTaskId(), StorageNodeID: req.GetStorageNodeId(), + WorkerID: req.GetWorkerId(), FencingToken: req.GetFencingToken(), + RevisionID: req.GetRevisionId(), ManifestSHA256: req.GetManifestSha256(), + ResultObjectID: req.GetResultObjectId(), ResultObjectSHA256: req.GetResultObjectSha256(), + PackageCount: req.GetPackageCount(), TotalBytes: req.GetTotalBytes(), + CompletionStatus: repositoryTaskStatusString(req.GetCompletionStatus()), + ErrorCode: req.GetErrorCode(), ErrorMessage: req.GetErrorMessage(), + }) + if err != nil { + return nil, err + } + return &repositorynodepb.PrepareTaskCommitResponse{ + CommitToken: task.CommitToken, RevisionId: task.PreparedRevisionID, + ManifestSha256: task.PreparedManifestSHA256, State: task.CommitState, + }, nil +} + +func (c *RepositoryNodeInternalController) ConfirmTaskCommit(ctx context.Context, + req *repositorynodepb.ConfirmTaskCommitRequest, +) (*emptypb.Empty, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + if err := c.authenticateNode(ctx, req.GetStorageNodeId()); err != nil { + return nil, err + } + _, err := c.application.ConfirmRepositoryNodeTaskCommit(request.ConfirmRepositoryTaskCommitRequest{ + TaskID: req.GetTaskId(), StorageNodeID: req.GetStorageNodeId(), WorkerID: req.GetWorkerId(), + FencingToken: req.GetFencingToken(), CommitToken: req.GetCommitToken(), + }) + if err != nil { + return nil, err + } + return &emptypb.Empty{}, nil +} + +func (c *RepositoryNodeInternalController) RenewNodeCertificate(ctx context.Context, + req *repositorynodepb.RenewNodeCertificateRequest, +) (*repositorynodepb.NodeCertificateResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + if err := c.authenticateNode(ctx, req.GetStorageNodeId()); err != nil { + return nil, err + } + bundle, err := c.application.RenewRepositoryStorageNodeCertificate(req.GetStorageNodeId(), req.GetCsrPem()) + if err != nil { + return nil, err + } + return repositoryNodeCertificateToPB(bundle), nil +} + +func repositoryNodeCertificateToPB(value *request.RepositoryNodeCertificateBundle) *repositorynodepb.NodeCertificateResponse { + if value == nil { + return nil + } + return &repositorynodepb.NodeCertificateResponse{StorageNodeId: value.NodeID, + CertificatePem: value.CertificatePEM, CaCertificatePem: value.CACertificatePEM, + SerialNumber: value.SerialNumber, Fingerprint: value.Fingerprint, + NotBefore: timestamppb.New(value.NotBefore), NotAfter: timestamppb.New(value.NotAfter)} +} + +func repositoryTaskStatusString(value repositorypb.RepositoryTaskStatus) string { + switch value { + case repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_SUCCESS: + return request.RepositoryTaskStatusSuccess + case repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_PARTIAL_SUCCESS: + return request.RepositoryTaskStatusPartialSuccess + default: + return "" + } +} + +func (c *RepositoryNodeInternalController) authenticateNode(ctx context.Context, storageNodeID string) error { + fingerprint, ok := repositoryNodeCertificateFingerprint(ctx) + if !ok { + return application.ErrRepositoryNodeUnauthorized + } + return c.application.AuthenticateRepositoryStorageNode(storageNodeID, fingerprint) +} diff --git a/manager/backend/services/footstone/presentation/repository_node_internal_controller_gpg_test.go b/manager/backend/services/footstone/presentation/repository_node_internal_controller_gpg_test.go new file mode 100644 index 0000000000000000000000000000000000000000..5dac05b9e9fb8bde3bb4ed39b30ea9f55e8c81d2 --- /dev/null +++ b/manager/backend/services/footstone/presentation/repository_node_internal_controller_gpg_test.go @@ -0,0 +1,28 @@ +package presentation + +import ( + "reflect" + "testing" + + repositorynodepb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repositorynode" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +func TestRepositoryGPGKeyReferences(t *testing.T) { + keys := []po.RepositoryGPGKey{ + {KeyID: "0123456789ABCDEF", Fingerprint: "0123456789ABCDEF0123456789ABCDEF01234567", + PublicKeyPath: "keys/repository-a/0123456789ABCDEF0123456789ABCDEF01234567/public.key"}, + {KeyID: "FEDCBA9876543210", Fingerprint: "FEDCBA9876543210FEDCBA9876543210FEDCBA98", + PublicKeyPath: "keys/repository-a/FEDCBA9876543210FEDCBA9876543210FEDCBA98/public.key"}, + } + want := []*repositorynodepb.RepositoryGPGKeyReference{ + {KeyId: keys[0].KeyID, Fingerprint: keys[0].Fingerprint, PublicKeyPath: keys[0].PublicKeyPath}, + {KeyId: keys[1].KeyID, Fingerprint: keys[1].Fingerprint, PublicKeyPath: keys[1].PublicKeyPath}, + } + if got := repositoryGPGKeyReferences(keys); !reflect.DeepEqual(got, want) { + t.Fatalf("repositoryGPGKeyReferences() = %#v, want %#v", got, want) + } + if got := repositoryGPGKeyReferences(nil); len(got) != 0 || got == nil { + t.Fatalf("repositoryGPGKeyReferences(nil) = %#v, want a non-nil empty slice", got) + } +} diff --git a/manager/backend/services/footstone/presentation/repository_node_internal_server.go b/manager/backend/services/footstone/presentation/repository_node_internal_server.go new file mode 100644 index 0000000000000000000000000000000000000000..06becc7a6a075befc6374d9360bedb303e459df6 --- /dev/null +++ b/manager/backend/services/footstone/presentation/repository_node_internal_server.go @@ -0,0 +1,141 @@ +package presentation + +import ( + "context" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "encoding/hex" + "errors" + "fmt" + "net" + "os" + "strings" + "time" + + repositorynodepb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repositorynode" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/application" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "trpc.group/trpc-go/trpc-go" + "trpc.group/trpc-go/trpc-go/server" + "trpc.group/trpc-go/trpc-go/transport" +) + +const repositoryNodeInternalServiceName = "trpc.ocmanager.footstone.repositorynode" + +func registerRepositoryNodeInternalService(s *server.Server, app application.Application, + cfg config.RepositoryInternalControlConfig, +) error { + if !cfg.Enabled { + return nil + } + listener, err := newRepositoryNodeIdentityListener(cfg) + if err != nil { + return err + } + service := server.New( + server.WithServiceName(repositoryNodeInternalServiceName), + server.WithAddress(cfg.ListenAddress), + server.WithNetwork("tcp"), + server.WithProtocol("trpc"), + server.WithTimeout(60*time.Second), + server.WithListener(listener), + ) + s.AddService(repositoryNodeInternalServiceName, service) + repositorynodepb.RegisterRepositoryNodeInternalServiceService(service, + NewRepositoryNodeInternalController(app)) + return nil +} + +func newRepositoryNodeIdentityListener(cfg config.RepositoryInternalControlConfig) (net.Listener, error) { + cfg.ListenAddress = strings.TrimSpace(cfg.ListenAddress) + if cfg.ListenAddress == "" || cfg.ServerCert == "" || cfg.ServerKey == "" || cfg.ClientCA == "" { + return nil, errors.New("repository node internal mTLS listener configuration is incomplete") + } + certificate, err := tls.LoadX509KeyPair(cfg.ServerCert, cfg.ServerKey) + if err != nil { + return nil, fmt.Errorf("load repository node internal server certificate: %w", err) + } + caPEM, err := os.ReadFile(cfg.ClientCA) + if err != nil { + return nil, fmt.Errorf("read repository node client CA: %w", err) + } + clientCAs := x509.NewCertPool() + if !clientCAs.AppendCertsFromPEM(caPEM) { + return nil, errors.New("repository node client CA does not contain a valid certificate") + } + raw, err := net.Listen("tcp", cfg.ListenAddress) + if err != nil { + return nil, fmt.Errorf("listen for repository node internal RPC: %w", err) + } + tlsListener := tls.NewListener(raw, &tls.Config{ + Certificates: []tls.Certificate{certificate}, ClientCAs: clientCAs, + ClientAuth: tls.RequireAndVerifyClientCert, MinVersion: tls.VersionTLS12, + }) + return &repositoryNodeIdentityListener{Listener: tlsListener, handshakeTimeout: 10 * time.Second}, nil +} + +type repositoryNodeIdentityListener struct { + net.Listener + handshakeTimeout time.Duration +} + +func (l *repositoryNodeIdentityListener) Accept() (net.Conn, error) { + for { + conn, err := l.Listener.Accept() + if err != nil { + return nil, err + } + tlsConn, ok := conn.(*tls.Conn) + if !ok { + conn.Close() + continue + } + ctx, cancel := context.WithTimeout(context.Background(), l.handshakeTimeout) + err = tlsConn.HandshakeContext(ctx) + cancel() + if err != nil { + tlsConn.Close() + continue + } + state := tlsConn.ConnectionState() + if len(state.PeerCertificates) < 1 { + tlsConn.Close() + continue + } + digest := sha256.Sum256(state.PeerCertificates[0].Raw) + return &repositoryNodeIdentityConn{Conn: tlsConn, remote: repositoryNodeIdentityAddr{ + Addr: tlsConn.RemoteAddr(), fingerprint: hex.EncodeToString(digest[:]), + }}, nil + } +} + +type repositoryNodeIdentityConn struct { + net.Conn + remote repositoryNodeIdentityAddr +} + +func (c *repositoryNodeIdentityConn) RemoteAddr() net.Addr { return c.remote } + +type repositoryNodeIdentityAddr struct { + net.Addr + fingerprint string +} + +func (a repositoryNodeIdentityAddr) CertificateFingerprint() string { return a.fingerprint } + +func repositoryNodeCertificateFingerprint(ctx context.Context) (string, bool) { + var addr net.Addr + if message := trpc.Message(ctx); message != nil { + addr = message.RemoteAddr() + } + if addr == nil { + addr = transport.RemoteAddrFromContext(ctx) + } + identity, ok := addr.(interface{ CertificateFingerprint() string }) + if !ok { + return "", false + } + fingerprint := identity.CertificateFingerprint() + return fingerprint, len(fingerprint) == sha256.Size*2 +} diff --git a/manager/backend/services/footstone/presentation/repository_node_internal_server_test.go b/manager/backend/services/footstone/presentation/repository_node_internal_server_test.go new file mode 100644 index 0000000000000000000000000000000000000000..a6fea1801835379adb4f80be77c831d4f08fe710 --- /dev/null +++ b/manager/backend/services/footstone/presentation/repository_node_internal_server_test.go @@ -0,0 +1,268 @@ +package presentation + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/hex" + "encoding/pem" + "errors" + "math/big" + "net" + "os" + "path/filepath" + "strings" + "testing" + "time" + + repositorynodepb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repositorynode" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "trpc.group/trpc-go/trpc-go/client" + "trpc.group/trpc-go/trpc-go/server" + "trpc.group/trpc-go/trpc-go/transport" +) + +func TestRepositoryNodeCertificateFingerprintFromContext(t *testing.T) { + want := strings.Repeat("ab", 32) + ctx := context.WithValue(context.Background(), transport.RemoteAddrContextKey, + repositoryNodeIdentityAddr{Addr: testRepositoryNodeAddr("127.0.0.1:1234"), fingerprint: want}) + got, ok := repositoryNodeCertificateFingerprint(ctx) + if !ok || got != want { + t.Fatalf("repositoryNodeCertificateFingerprint() = %q/%v", got, ok) + } + if _, ok = repositoryNodeCertificateFingerprint(context.Background()); ok { + t.Fatal("context without a verified mTLS identity was accepted") + } +} + +func TestRepositoryNodeIdentityListenerRequiresMTLSAndExportsFingerprint(t *testing.T) { + pki := createRepositoryNodeTestPKI(t) + listener, err := newRepositoryNodeIdentityListener(config.RepositoryInternalControlConfig{ + ListenAddress: "127.0.0.1:0", ServerCert: pki.serverCertPath, + ServerKey: pki.serverKeyPath, ClientCA: pki.caPath, + }) + if err != nil { + t.Fatal(err) + } + defer listener.Close() + accepted := make(chan net.Conn, 1) + acceptErrors := make(chan error, 1) + go func() { + conn, acceptErr := listener.Accept() + if acceptErr != nil { + acceptErrors <- acceptErr + return + } + accepted <- conn + }() + + withoutCertificate := &tls.Config{RootCAs: pki.roots, ServerName: "repository-internal.test", + MinVersion: tls.VersionTLS12} + if conn, dialErr := tls.Dial("tcp", listener.Addr().String(), withoutCertificate); dialErr == nil { + _ = conn.SetDeadline(time.Now().Add(time.Second)) + _, _ = conn.Write([]byte{0}) + _, dialErr = conn.Read(make([]byte, 1)) + conn.Close() + if dialErr == nil { + t.Fatal("client without a certificate did not receive a TLS rejection") + } + } + select { + case conn := <-accepted: + conn.Close() + t.Fatal("mTLS listener exported an identity for a client without a certificate") + case err = <-acceptErrors: + t.Fatal(err) + case <-time.After(100 * time.Millisecond): + } + withCertificate := withoutCertificate.Clone() + withCertificate.Certificates = []tls.Certificate{pki.clientCertificate} + client, err := tls.Dial("tcp", listener.Addr().String(), withCertificate) + if err != nil { + t.Fatalf("dial mTLS listener: %v", err) + } + defer client.Close() + select { + case err = <-acceptErrors: + t.Fatal(err) + case serverConn := <-accepted: + defer serverConn.Close() + identity, ok := serverConn.RemoteAddr().(interface{ CertificateFingerprint() string }) + if !ok { + t.Fatalf("accepted connection address type = %T", serverConn.RemoteAddr()) + } + digest := sha256.Sum256(pki.clientLeafDER) + if got, want := identity.CertificateFingerprint(), hex.EncodeToString(digest[:]); got != want { + t.Fatalf("client certificate fingerprint = %s, want %s", got, want) + } + case <-time.After(5 * time.Second): + t.Fatal("timed out waiting for mTLS identity connection") + } +} + +func TestRepositoryNodeTRPCPreservesMTLSIdentity(t *testing.T) { + pki := createRepositoryNodeTestPKI(t) + listener, err := newRepositoryNodeIdentityListener(config.RepositoryInternalControlConfig{ + ListenAddress: "127.0.0.1:0", ServerCert: pki.serverCertPath, + ServerKey: pki.serverKeyPath, ClientCA: pki.caPath, + }) + if err != nil { + t.Fatal(err) + } + service := server.New( + server.WithServiceName(repositoryNodeInternalServiceName), + server.WithAddress(listener.Addr().String()), server.WithNetwork("tcp"), + server.WithProtocol("trpc"), server.WithListener(listener), + ) + probe := &repositoryNodeFingerprintProbe{fingerprints: make(chan string, 1)} + repositorynodepb.RegisterRepositoryNodeInternalServiceService(service, probe) + serveErrors := make(chan error, 1) + go func() { serveErrors <- service.Serve() }() + defer service.Close(nil) + time.Sleep(100 * time.Millisecond) + + proxy := repositorynodepb.NewRepositoryNodeInternalServiceClientProxy( + client.WithTarget("ip://"+listener.Addr().String()), + client.WithTLS(pki.clientCertPath, pki.clientKeyPath, pki.caPath, "repository-internal.test"), + client.WithTimeout(3*time.Second), + ) + response, err := proxy.HeartbeatNode(context.Background(), &repositorynodepb.HeartbeatNodeRequest{ + StorageNodeId: "11111111-1111-4111-8111-111111111111", + }) + if err != nil || !response.GetNodeEnabled() { + t.Fatalf("HeartbeatNode() response/error = %#v/%v", response, err) + } + select { + case fingerprint := <-probe.fingerprints: + digest := sha256.Sum256(pki.clientLeafDER) + if want := hex.EncodeToString(digest[:]); fingerprint != want { + t.Fatalf("handler fingerprint = %s, want %s", fingerprint, want) + } + case err = <-serveErrors: + t.Fatalf("tRPC service exited early: %v", err) + case <-time.After(3 * time.Second): + t.Fatal("timed out waiting for tRPC handler identity") + } +} + +type testRepositoryNodeAddr string + +func (a testRepositoryNodeAddr) Network() string { return "tcp" } +func (a testRepositoryNodeAddr) String() string { return string(a) } + +var _ net.Addr = testRepositoryNodeAddr("") + +type repositoryNodeTestPKI struct { + caPath string + serverCertPath string + serverKeyPath string + clientCertPath string + clientKeyPath string + roots *x509.CertPool + clientCertificate tls.Certificate + clientLeafDER []byte +} + +func createRepositoryNodeTestPKI(t *testing.T) repositoryNodeTestPKI { + t.Helper() + directory := t.TempDir() + now := time.Now().UTC() + caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + caTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "repository-test-ca"}, + NotBefore: now.Add(-time.Hour), NotAfter: now.Add(time.Hour), IsCA: true, + BasicConstraintsValid: true, KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + } + caDER := createRepositoryNodeTestCertificate(t, caTemplate, caTemplate, &caKey.PublicKey, caKey) + caPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caDER}) + caPath := filepath.Join(directory, "ca.pem") + writeRepositoryNodeTestFile(t, caPath, caPEM, 0o644) + + serverTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(2), Subject: pkix.Name{CommonName: "repository-internal.test"}, + DNSNames: []string{"repository-internal.test"}, NotBefore: now.Add(-time.Hour), NotAfter: now.Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + } + serverCertPath, serverKeyPath, _ := createRepositoryNodeTestLeaf(t, directory, "server", + serverTemplate, caTemplate, caKey) + clientTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(3), Subject: pkix.Name{CommonName: "repository-node-test"}, + NotBefore: now.Add(-time.Hour), NotAfter: now.Add(time.Hour), KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + } + clientCertPath, clientKeyPath, clientDER := createRepositoryNodeTestLeaf(t, directory, "client", + clientTemplate, caTemplate, caKey) + clientCertificate, err := tls.LoadX509KeyPair(clientCertPath, clientKeyPath) + if err != nil { + t.Fatal(err) + } + roots := x509.NewCertPool() + if !roots.AppendCertsFromPEM(caPEM) { + t.Fatal("append test CA") + } + return repositoryNodeTestPKI{caPath: caPath, serverCertPath: serverCertPath, + serverKeyPath: serverKeyPath, clientCertPath: clientCertPath, clientKeyPath: clientKeyPath, + roots: roots, clientCertificate: clientCertificate, clientLeafDER: clientDER} +} + +type repositoryNodeFingerprintProbe struct { + repositorynodepb.UnimplementedRepositoryNodeInternalService + fingerprints chan string +} + +func (p *repositoryNodeFingerprintProbe) HeartbeatNode(ctx context.Context, + _ *repositorynodepb.HeartbeatNodeRequest, +) (*repositorynodepb.HeartbeatNodeResponse, error) { + fingerprint, ok := repositoryNodeCertificateFingerprint(ctx) + if !ok { + return nil, errors.New("verified client certificate identity is missing") + } + p.fingerprints <- fingerprint + return &repositorynodepb.HeartbeatNodeResponse{NodeEnabled: true}, nil +} + +func createRepositoryNodeTestLeaf(t *testing.T, directory, name string, template, ca *x509.Certificate, + caKey *ecdsa.PrivateKey, +) (string, string, []byte) { + t.Helper() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + der := createRepositoryNodeTestCertificate(t, template, ca, &key.PublicKey, caKey) + keyDER, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + t.Fatal(err) + } + certPath := filepath.Join(directory, name+".pem") + keyPath := filepath.Join(directory, name+".key") + writeRepositoryNodeTestFile(t, certPath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), 0o644) + writeRepositoryNodeTestFile(t, keyPath, pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}), 0o600) + return certPath, keyPath, der +} + +func createRepositoryNodeTestCertificate(t *testing.T, template, parent *x509.Certificate, publicKey any, + privateKey any, +) []byte { + t.Helper() + der, err := x509.CreateCertificate(rand.Reader, template, parent, publicKey, privateKey) + if err != nil { + t.Fatal(err) + } + return der +} + +func writeRepositoryNodeTestFile(t *testing.T, path string, content []byte, mode os.FileMode) { + t.Helper() + if err := os.WriteFile(path, content, mode); err != nil { + t.Fatal(err) + } +} diff --git a/manager/backend/services/footstone/presentation/repository_node_recovery_transport_test.go b/manager/backend/services/footstone/presentation/repository_node_recovery_transport_test.go new file mode 100644 index 0000000000000000000000000000000000000000..45cbc228e802505b61759320e1f005a070469328 --- /dev/null +++ b/manager/backend/services/footstone/presentation/repository_node_recovery_transport_test.go @@ -0,0 +1,217 @@ +package presentation + +import ( + "context" + "fmt" + "net" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + repositorynodepb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repositorynode" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/repositoryworker" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/repositoryworker/controlplane" + "google.golang.org/protobuf/types/known/emptypb" + "trpc.group/trpc-go/trpc-go/server" +) + +// The server fixture supplies task state, but every control-plane call uses the +// production client over a real TCP/mTLS connection, including reconnects. +func TestRepositoryWorkerRecoversRealTRPCConnection(t *testing.T) { + pki := createRepositoryNodeTestPKI(t) + fixture := &recoveryTransportFixture{confirmed: make(chan string, 2)} + start := func(address string) (func(), string) { + t.Helper() + listener, err := newRepositoryNodeIdentityListener(config.RepositoryInternalControlConfig{ + ListenAddress: address, ServerCert: pki.serverCertPath, ServerKey: pki.serverKeyPath, ClientCA: pki.caPath, + }) + if err != nil { + t.Fatal(err) + } + tracked := &recoveryTransportListener{Listener: listener} + service := server.New(server.WithServiceName(repositoryNodeInternalServiceName), + server.WithAddress(listener.Addr().String()), server.WithNetwork("tcp"), + server.WithProtocol("trpc"), server.WithListener(tracked)) + repositorynodepb.RegisterRepositoryNodeInternalServiceService(service, fixture) + go func() { _ = service.Serve() }() + return func() { tracked.Close(); service.Close(nil) }, listener.Addr().String() + } + stopServer, address := start("127.0.0.1:0") + plane, err := controlplane.New(config.RepositoryNodeClientConfig{ + StorageNodeID: recoveryTransportNodeID, ControlTarget: "ip://" + address, + ClientCert: pki.clientCertPath, ClientKey: pki.clientKeyPath, ServerCA: pki.caPath, + ServerName: "repository-internal.test", HeartbeatSeconds: 1, RPCSeconds: 1, + }, t.TempDir()) + if err != nil { + stopServer() + t.Fatal(err) + } + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + if _, err = plane.HeartbeatNode(ctx, controlplane.NodeHeartbeat{}); err != nil { + stopServer() + t.Fatal(err) + } + stopServer() + if _, err = plane.HeartbeatNode(ctx, controlplane.NodeHeartbeat{}); err == nil { + t.Fatal("closed control-plane connection unexpectedly accepted heartbeat") + } + observed := &observedTransportPlane{Plane: plane, failed: make(chan struct{}, 1)} + executor := &recoveryTransportExecutor{} + worker, err := repositoryworker.NewRemoteWorker(recoveryTransportNodeID, "transport-recovery", observed, + recoveryTransportProbe{}, executor, 15*time.Second, 20*time.Millisecond, 20*time.Millisecond) + if err != nil { + t.Fatal(err) + } + done := make(chan error, 1) + go func() { done <- worker.Run(ctx) }() + defer func() { + cancel() + select { + case <-done: + case <-time.After(3 * time.Second): + t.Error("worker did not stop") + } + }() + select { + case <-observed.failed: + case <-ctx.Done(): + t.Fatal("worker never observed actual transport failure") + } + if observed.claims.Load() != 0 { + t.Fatal("worker claimed before startup heartbeat recovered") + } + stopServer, _ = start(address) + defer func() { cancel(); stopServer() }() + for i := 0; i < 2; i++ { + select { + case id := <-fixture.confirmed: + t.Logf("same-client reconnect confirmed task=%s", id) + case <-done: + t.Fatal("worker exited before recovered tasks completed") + case <-ctx.Done(): + t.Fatal("control client did not reconnect and complete tasks") + } + } + if executor.executed.Load() != 2 || executor.activated.Load() != 2 || fixture.prepares.Load() != 2 || fixture.confirms.Load() != 2 { + t.Fatalf("execute/activate/prepare/confirm = %d/%d/%d/%d", executor.executed.Load(), executor.activated.Load(), fixture.prepares.Load(), fixture.confirms.Load()) + } + t.Log("real TCP/mTLS failure, startup heartbeat gate, same-port recovery, and two subsequent commits PASS") +} + +const recoveryTransportNodeID = "71111111-1111-4111-8111-111111111111" + +type recoveryTransportListener struct { + net.Listener + mu sync.Mutex + connections []net.Conn + closed bool +} + +func (l *recoveryTransportListener) Accept() (net.Conn, error) { + conn, err := l.Listener.Accept() + if err == nil { + l.mu.Lock() + if l.closed { + _ = conn.Close() + } + l.connections = append(l.connections, conn) + l.mu.Unlock() + } + return conn, err +} + +func (l *recoveryTransportListener) Close() error { + err := l.Listener.Close() + l.mu.Lock() + defer l.mu.Unlock() + l.closed = true + for _, conn := range l.connections { + _ = conn.Close() + } + return err +} + +type observedTransportPlane struct { + controlplane.Plane + failed chan struct{} + claims atomic.Int32 +} + +func (p *observedTransportPlane) HeartbeatNode(ctx context.Context, value controlplane.NodeHeartbeat) (*repositorynodepb.HeartbeatNodeResponse, error) { + response, err := p.Plane.HeartbeatNode(ctx, value) + if err != nil { + select { + case p.failed <- struct{}{}: + default: + } + } + return response, err +} + +func (p *observedTransportPlane) ClaimTask(ctx context.Context, worker string, lease int32) (*repositorynodepb.RepositoryTaskSpec, error) { + p.claims.Add(1) + return p.Plane.ClaimTask(ctx, worker, lease) +} + +type recoveryTransportFixture struct { + repositorynodepb.UnimplementedRepositoryNodeInternalService + claims atomic.Int32 + prepares atomic.Int32 + confirms atomic.Int32 + confirmed chan string +} + +func (f *recoveryTransportFixture) HeartbeatNode(ctx context.Context, _ *repositorynodepb.HeartbeatNodeRequest) (*repositorynodepb.HeartbeatNodeResponse, error) { + if _, ok := repositoryNodeCertificateFingerprint(ctx); !ok { + return nil, fmt.Errorf("mTLS identity missing") + } + return &repositorynodepb.HeartbeatNodeResponse{NodeEnabled: true}, nil +} + +func (f *recoveryTransportFixture) ClaimTask(_ context.Context, r *repositorynodepb.ClaimTaskRequest) (*repositorynodepb.ClaimTaskResponse, error) { + n := f.claims.Add(1) + if n > 2 { + return &repositorynodepb.ClaimTaskResponse{}, nil + } + return &repositorynodepb.ClaimTaskResponse{Task: &repositorynodepb.RepositoryTaskSpec{ + TaskId: fmt.Sprintf("72222222-2222-4222-8222-%012d", n), + RepositoryId: "73333333-3333-4333-8333-333333333333", StorageNodeId: r.StorageNodeId, + TaskType: repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_UPLOAD, FencingToken: uint64(n), + }}, nil +} + +func (f *recoveryTransportFixture) PrepareTaskCommit(_ context.Context, r *repositorynodepb.PrepareTaskCommitRequest) (*repositorynodepb.PrepareTaskCommitResponse, error) { + f.prepares.Add(1) + return &repositorynodepb.PrepareTaskCommitResponse{RevisionId: r.RevisionId, ManifestSha256: r.ManifestSha256, CommitToken: "transport-test-commit"}, nil +} + +func (f *recoveryTransportFixture) ConfirmTaskCommit(_ context.Context, r *repositorynodepb.ConfirmTaskCommitRequest) (*emptypb.Empty, error) { + f.confirms.Add(1) + f.confirmed <- r.TaskId + return &emptypb.Empty{}, nil +} + +type recoveryTransportProbe struct{} + +func (recoveryTransportProbe) Probe(context.Context) (controlplane.NodeHeartbeat, error) { + return controlplane.NodeHeartbeat{}, nil +} + +type recoveryTransportExecutor struct{ executed, activated atomic.Int32 } + +func (e *recoveryTransportExecutor) Execute(context.Context, *repositorynodepb.RepositoryTaskSpec) (repositoryworker.RemoteOutcome, error) { + e.executed.Add(1) + return repositoryworker.RemoteOutcome{RevisionID: "74444444-4444-4444-8444-444444444444", + ManifestSHA256: strings.Repeat("a", 64), ResultObjectID: "test-result", ResultObjectSHA256: strings.Repeat("b", 64), + CompletionStatus: repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_SUCCESS}, nil +} + +func (e *recoveryTransportExecutor) Activate(context.Context, *repositorynodepb.RepositoryTaskSpec, repositoryworker.RemoteOutcome) error { + e.activated.Add(1) + return nil +} diff --git a/manager/backend/services/footstone/presentation/repository_ocai_controller.go b/manager/backend/services/footstone/presentation/repository_ocai_controller.go new file mode 100644 index 0000000000000000000000000000000000000000..a520964a6de7cf1a8798cb27076dadaab926be98 --- /dev/null +++ b/manager/backend/services/footstone/presentation/repository_ocai_controller.go @@ -0,0 +1,111 @@ +package presentation + +import ( + "context" + "fmt" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func (c *RepositoryController) BuildRepositoryOCAIContext(ctx context.Context, + req *repositorypb.BuildRepositoryOCAIContextRequest, +) (*repositorypb.RepositoryOCAIContextResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + value, err := c.application.BuildRepositoryOCAIContext(c.ToContext(ctx), req.GetTaskId()) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryOCAIContextResponse{Context: repositoryOCAIContextToPB(value)}, nil +} + +func (c *RepositoryController) CreateRepositoryOCAIAnalysis(ctx context.Context, + req *repositorypb.CreateRepositoryOCAIAnalysisRequest, +) (*repositorypb.RepositoryOCAIAnalysisResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + value, err := c.application.CreateRepositoryOCAIAnalysis(c.ToContext(ctx), req.GetTaskId()) + if err != nil { + return nil, err + } + return repositoryOCAIAnalysisResultToPB(value), nil +} + +func (c *RepositoryController) DescribeRepositoryOCAIAnalysis(ctx context.Context, + req *repositorypb.DescribeRepositoryOCAIAnalysisRequest, +) (*repositorypb.RepositoryOCAIAnalysisResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + value, err := c.application.DescribeRepositoryOCAIAnalysis(c.ToContext(ctx), req.GetAnalysisId()) + if err != nil { + return nil, err + } + return repositoryOCAIAnalysisResultToPB(value), nil +} + +func (c *RepositoryController) AppendRepositoryOCAIFeedback(ctx context.Context, + req *repositorypb.AppendRepositoryOCAIFeedbackRequest, +) (*repositorypb.RepositoryOCAIAnalysisResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + value, err := c.application.AppendRepositoryOCAIFeedback(c.ToContext(ctx), req.GetSessionId(), + req.GetActionTaskId(), req.GetActionResult()) + if err != nil { + return nil, err + } + return repositoryOCAIAnalysisResultToPB(value), nil +} + +func repositoryOCAIAnalysisResultToPB(value *request.RepositoryOCAIAnalysisResult) *repositorypb.RepositoryOCAIAnalysisResponse { + if value == nil { + return &repositorypb.RepositoryOCAIAnalysisResponse{} + } + return &repositorypb.RepositoryOCAIAnalysisResponse{ + Analysis: repositoryOCAIAnalysisToPB(value.Analysis), + Context: repositoryOCAIContextToPB(value.Context), Instruction: value.Instruction, + } +} + +func repositoryOCAIAnalysisToPB(value *po.RepositoryAIAnalysis) *repositorypb.RepositoryOCAIAnalysis { + if value == nil { + return nil + } + return &repositorypb.RepositoryOCAIAnalysis{ + Id: value.ID, TaskId: value.TaskID, SessionId: value.SessionID, + ContextSha256: value.ContextSHA256, Status: value.Status, + ActionTaskId: value.ActionTaskID, ActionResult: value.ActionResult, + RequestedBy: value.RequestedBy, CreatedAt: timestamppb.New(value.CreatedAt), + FeedbackAt: optionalTimestamp(value.FeedbackAt), + } +} + +func repositoryOCAIContextToPB(value *request.RepositoryOCAIContext) *repositorypb.RepositoryOCAIContext { + if value == nil { + return nil + } + result := &repositorypb.RepositoryOCAIContext{ + TaskId: value.TaskID, RepositoryId: value.RepositoryID, RepositoryName: value.RepositoryName, + SystemProduct: value.SystemProduct, ProductVersion: value.ProductVersion, + Architecture: value.Architecture, TaskType: value.TaskType, TaskStatus: value.TaskStatus, + StorageNodeId: value.StorageNodeID, RevisionId: value.RevisionID, CurrentStep: value.CurrentStep, + ErrorCode: value.ErrorCode, ErrorMessage: value.ErrorMessage, LogSummary: value.LogSummary, + ContextJson: value.ContextJSON, ContextSha256: value.ContextSHA256, Truncated: value.Truncated, + } + if value.Diagnosis != nil { + result.Diagnosis = &repositorypb.RepositoryDiagnosis{ + FailureCategory: value.Diagnosis.FailureCategory, ErrorCode: value.Diagnosis.ErrorCode, + FailedStep: value.Diagnosis.FailedStep, LikelyCause: value.Diagnosis.LikelyCause, + Evidence: append([]string(nil), value.Diagnosis.Evidence...), + RecommendedAction: value.Diagnosis.RecommendedAction, RuleId: value.Diagnosis.RuleID, + GeneratedAt: timestamppb.New(value.Diagnosis.GeneratedAt), + } + } + return result +} diff --git a/manager/backend/services/footstone/presentation/repository_package_install_controller.go b/manager/backend/services/footstone/presentation/repository_package_install_controller.go new file mode 100644 index 0000000000000000000000000000000000000000..1adee026396418dca1128d14f0423185ff480958 --- /dev/null +++ b/manager/backend/services/footstone/presentation/repository_package_install_controller.go @@ -0,0 +1,99 @@ +package presentation + +import ( + "context" + "fmt" + "strconv" + "time" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func (c *RepositoryController) PreviewRepositoryPackageInstall(ctx context.Context, + req *repositorypb.PreviewRepositoryPackageInstallRequest, +) (*repositorypb.RepositoryPackageInstallPreviewResponse, error) { + value, err := c.application.PreviewRepositoryPackageInstall(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + result := &repositorypb.RepositoryPackageInstallPreview{ + PreviewId: value.Preview.ID, PreviewDigest: value.Preview.PreviewDigest, + RepositoryId: value.Payload.RepositoryID, RevisionId: value.Payload.RevisionID, + RepoId: value.Payload.RepoID, PackageNames: value.Payload.PackageNames, + Status: value.Preview.Status, ExpiresAt: timestamppb.New(value.Preview.ExpiresAt), + CreatedBy: value.Preview.CreatedBy, + } + for _, target := range value.Payload.Targets { + result.Targets = append(result.Targets, repositoryPackageInstallPreviewTargetToPB(target)) + } + return &repositorypb.RepositoryPackageInstallPreviewResponse{Preview: result}, nil +} + +func repositoryPackageInstallPreviewTargetToPB(target request.RepositoryPackageInstallTarget) *repositorypb.RepositoryPackageInstallPreviewTarget { + return &repositorypb.RepositoryPackageInstallPreviewTarget{ + AgentId: strconv.FormatUint(target.AgentID, 10), Ip: target.IP, + Status: taskStatusToPB(target.Status), ExitCode: target.ExitCode, + ErrorCode: target.ErrorCode, ErrorMessage: target.ErrorMessage, + RawOutput: target.RawOutput, OutputTruncated: target.OutputTruncated, + SelectedPackages: append([]string(nil), target.SelectedPackages...), + DependencyPackages: append([]string(nil), target.DependencyPackages...), + UpgradePackages: append([]string(nil), target.UpgradePackages...), + ReplacementPackages: append([]string(nil), target.ReplacementPackages...), + DownloadBytes: target.DownloadBytes, DiskChangeBytes: target.DiskChangeBytes, + StartedAt: nonZeroTimestamp(target.StartedAt), FinishedAt: nonZeroTimestamp(target.FinishedAt), + } +} + +func (c *RepositoryController) ConfirmRepositoryPackageInstall(ctx context.Context, + req *repositorypb.ConfirmRepositoryPackageInstallRequest, +) (*repositorypb.RepositoryPackageInstallResponse, error) { + value, err := c.application.ConfirmRepositoryPackageInstall(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryPackageInstallResponse{ + Install: repositoryPackageInstallToPB(value.Install), Task: repositoryTaskToPB(value.Task), + }, nil +} + +func (c *RepositoryController) DescribeRepositoryPackageInstall(ctx context.Context, + req *repositorypb.DescribeRepositoryPackageInstallRequest, +) (*repositorypb.RepositoryPackageInstallResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + value, err := c.application.DescribeRepositoryPackageInstall(c.ToContext(ctx), req.GetInstallId()) + if err != nil { + return nil, err + } + task, err := c.application.DescribeRepositoryTask(c.ToContext(ctx), value.RepositoryTaskID) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryPackageInstallResponse{ + Install: repositoryPackageInstallToPB(value), Task: repositoryTaskToPB(task), + }, nil +} + +func repositoryPackageInstallToPB(value *po.RepositoryPackageInstall) *repositorypb.RepositoryPackageInstall { + if value == nil { + return nil + } + return &repositorypb.RepositoryPackageInstall{ + Id: value.ID, ProjectId: value.ProjectID, RepositoryId: value.RepositoryID, + RevisionId: value.RevisionID, PreviewId: value.PreviewID, + RepositoryTaskId: value.RepositoryTaskID, TmsTaskId: value.TMSTaskID, + Status: value.Status, TimeoutSeconds: int32(value.TimeoutSeconds), RequestedBy: value.RequestedBy, + CreatedAt: timestamppb.New(value.CreatedAt), UpdatedAt: timestamppb.New(value.UpdatedAt), + } +} + +func nonZeroTimestamp(value time.Time) *timestamppb.Timestamp { + if value.IsZero() { + return nil + } + return timestamppb.New(value) +} diff --git a/manager/backend/services/footstone/presentation/repository_package_install_controller_test.go b/manager/backend/services/footstone/presentation/repository_package_install_controller_test.go new file mode 100644 index 0000000000000000000000000000000000000000..fbe87ef1cc4af05946498e80a194e11ab4b35424 --- /dev/null +++ b/manager/backend/services/footstone/presentation/repository_package_install_controller_test.go @@ -0,0 +1,33 @@ +package presentation + +import ( + "reflect" + "testing" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" +) + +func TestRepositoryPackageInstallPreviewTargetToPBKeepsStructuredTransaction(t *testing.T) { + now := time.Date(2026, 10, 13, 6, 0, 0, 0, time.UTC) + target := request.RepositoryPackageInstallTarget{ + AgentID: 23, IP: "192.0.2.23", Status: request.RepositoryTaskStatusSuccess, + RawOutput: "Dependencies resolved.", + SelectedPackages: []string{"app-1.0-1.noarch"}, + DependencyPackages: []string{"dependency-2.0-1.x86_64"}, + UpgradePackages: []string{"rpm-4.16-2.x86_64"}, + ReplacementPackages: []string{"rpm-4.16-1.x86_64"}, + DownloadBytes: 2 << 20, DiskChangeBytes: 6 << 20, + StartedAt: now, FinishedAt: now.Add(time.Second), + } + got := repositoryPackageInstallPreviewTargetToPB(target) + if got.GetAgentId() != "23" || got.GetRawOutput() != target.RawOutput || + !reflect.DeepEqual(got.GetSelectedPackages(), target.SelectedPackages) || + !reflect.DeepEqual(got.GetDependencyPackages(), target.DependencyPackages) || + !reflect.DeepEqual(got.GetUpgradePackages(), target.UpgradePackages) || + !reflect.DeepEqual(got.GetReplacementPackages(), target.ReplacementPackages) || + got.GetDownloadBytes() != target.DownloadBytes || got.GetDiskChangeBytes() != target.DiskChangeBytes || + got.GetStartedAt().AsTime() != now || got.GetFinishedAt().AsTime() != now.Add(time.Second) { + t.Fatalf("repositoryPackageInstallPreviewTargetToPB() = %#v", got) + } +} diff --git a/manager/backend/services/footstone/presentation/repository_report_controller.go b/manager/backend/services/footstone/presentation/repository_report_controller.go new file mode 100644 index 0000000000000000000000000000000000000000..816d9c7a1969f7cf337d2b7734e7ebcc0872449f --- /dev/null +++ b/manager/backend/services/footstone/presentation/repository_report_controller.go @@ -0,0 +1,125 @@ +package presentation + +import ( + "context" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func (c *RepositoryController) ExportRepositoryInventory(ctx context.Context, + req *repositorypb.ExportRepositoryInventoryRequest, +) (*repositorypb.RepositoryExportResponse, error) { + value, err := c.application.ExportRepositoryInventory(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + return repositoryExportToPB(value), nil +} + +func (c *RepositoryController) PreviewRepositoryInventoryImport(ctx context.Context, + req *repositorypb.PreviewRepositoryInventoryImportRequest, +) (*repositorypb.RepositoryInventoryImportPreviewResponse, error) { + value, err := c.application.PreviewRepositoryInventoryImport(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + response := &repositorypb.RepositoryInventoryImportPreviewResponse{ + PreviewId: value.ID, SchemaVersion: value.SchemaVersion, PreviewDigest: value.PreviewDigest, + ResourceVersion: value.ResourceVersion, ExpiresAt: timestamppb.New(value.ExpiresAt), + Status: value.Status, Warnings: value.Warnings, + Items: make([]*repositorypb.RepositoryInventoryImportItem, 0, len(value.Items)), + } + for index := range value.Items { + response.Items = append(response.Items, repositoryInventoryImportItemToPB(&value.Items[index])) + } + return response, nil +} + +func (c *RepositoryController) ConfirmRepositoryInventoryImport(ctx context.Context, + req *repositorypb.ConfirmRepositoryInventoryImportRequest, +) (*repositorypb.RepositoryInventoryImportResponse, error) { + value, err := c.application.ConfirmRepositoryInventoryImport(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + response := &repositorypb.RepositoryInventoryImportResponse{Job: repositoryInventoryImportJobToPB(value.Job), + Repositories: make([]*repositorypb.Repository, 0, len(value.Repositories))} + for index := range value.Repositories { + item, convertErr := repositoryToPB(value.Repositories[index]) + if convertErr != nil { + return nil, convertErr + } + response.Repositories = append(response.Repositories, item) + } + return response, nil +} + +func repositoryInventoryImportItemToPB(value *request.RepositoryInventoryImportItem) *repositorypb.RepositoryInventoryImportItem { + if value == nil { + return nil + } + enabled := value.Spec.Enabled + return &repositorypb.RepositoryInventoryImportItem{ + SourceRepositoryId: value.SourceRepositoryID, SourceName: value.SourceName, + Action: value.Action, Message: value.Message, + Spec: &repositorypb.RepositorySpec{ + ProjectId: value.Spec.ProjectID, SystemProduct: value.Spec.SystemProduct, + ProductVersion: value.Spec.ProductVersion, Name: value.Spec.Name, + Description: value.Spec.Description, Architecture: architectureToPB(value.Spec.Architecture), + Type: typeToPB(value.Spec.Type), UpstreamUrl: value.Spec.UpstreamURL, + AllowUpload: value.Spec.AllowUpload, IncludePackages: value.Spec.IncludePackages, + ExcludePackages: value.Spec.ExcludePackages, GpgPolicy: gpgPolicyToPB(value.Spec.GPGPolicy), + Enabled: &enabled, PrimaryStorageNodeId: value.Spec.PrimaryStorageNodeID, + SyncMode: syncModeToPB(value.Spec.SyncMode), BandwidthLimitBytes: value.Spec.BandwidthLimitBytes, + MaxParallelDownloads: int32(value.Spec.MaxParallelDownloads), + MetadataSigningKeyRef: value.Spec.MetadataSigningKeyRef, + }, + } +} + +func repositoryInventoryImportJobToPB(value *po.RepositoryInventoryImportJob) *repositorypb.RepositoryInventoryImportJob { + if value == nil { + return nil + } + return &repositorypb.RepositoryInventoryImportJob{ + Id: value.ID, ProjectId: value.ProjectID, PreviewId: value.PreviewID, + RepositoryTaskId: value.RepositoryTaskID, + SchemaVersion: value.SchemaVersion, ConflictStrategy: value.ConflictStrategy, + Status: value.Status, ErrorCode: value.ErrorCode, ErrorMessage: value.ErrorMessage, + RequestedBy: value.RequestedBy, CreatedAt: timestamppb.New(value.CreatedAt), + FinishedAt: optionalTimestamp(value.FinishedAt), + } +} + +func (c *RepositoryController) ExportRepositoryValidationReport(ctx context.Context, + req *repositorypb.ExportRepositoryValidationReportRequest, +) (*repositorypb.RepositoryExportResponse, error) { + value, err := c.application.ExportRepositoryValidationReport(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + return repositoryExportToPB(value), nil +} + +func (c *RepositoryController) ExportRepositoryDistributionResult(ctx context.Context, + req *repositorypb.ExportRepositoryDistributionResultRequest, +) (*repositorypb.RepositoryExportResponse, error) { + value, err := c.application.ExportRepositoryDistributionResult(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + return repositoryExportToPB(value), nil +} + +func repositoryExportToPB(value *request.RepositoryExport) *repositorypb.RepositoryExportResponse { + if value == nil { + return nil + } + return &repositorypb.RepositoryExportResponse{ + FileName: value.FileName, ContentType: value.ContentType, + Sha256: value.SHA256, Content: value.Content, + } +} diff --git a/manager/backend/services/footstone/presentation/repository_schedule_controller.go b/manager/backend/services/footstone/presentation/repository_schedule_controller.go new file mode 100644 index 0000000000000000000000000000000000000000..cd9e7f48c5e94020352eaa4b32e790ac50e64e72 --- /dev/null +++ b/manager/backend/services/footstone/presentation/repository_schedule_controller.go @@ -0,0 +1,99 @@ +package presentation + +import ( + "context" + "fmt" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func (c *RepositoryController) UpsertRepositorySyncSchedule(ctx context.Context, + req *repositorypb.UpsertRepositorySyncScheduleRequest, +) (*repositorypb.RepositorySyncScheduleResponse, error) { + value, err := c.application.UpsertRepositorySyncSchedule(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + return &repositorypb.RepositorySyncScheduleResponse{Schedule: repositoryScheduleToPB(value)}, nil +} + +func (c *RepositoryController) DescribeRepositorySyncSchedule(ctx context.Context, + req *repositorypb.DescribeRepositorySyncScheduleRequest, +) (*repositorypb.RepositorySyncScheduleResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + value, err := c.application.DescribeRepositorySyncSchedule(c.ToContext(ctx), req.GetRepositoryId()) + if err != nil { + return nil, err + } + return &repositorypb.RepositorySyncScheduleResponse{Schedule: repositoryScheduleToPB(value)}, nil +} + +func (c *RepositoryController) PauseRepositorySyncSchedule(ctx context.Context, + req *repositorypb.ChangeRepositorySyncScheduleStatusRequest, +) (*repositorypb.RepositorySyncScheduleResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + value, err := c.application.PauseRepositorySyncSchedule(c.ToContext(ctx), req.GetRepositoryId()) + if err != nil { + return nil, err + } + return &repositorypb.RepositorySyncScheduleResponse{Schedule: repositoryScheduleToPB(value)}, nil +} + +func (c *RepositoryController) ResumeRepositorySyncSchedule(ctx context.Context, + req *repositorypb.ChangeRepositorySyncScheduleStatusRequest, +) (*repositorypb.RepositorySyncScheduleResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + value, err := c.application.ResumeRepositorySyncSchedule(c.ToContext(ctx), req.GetRepositoryId()) + if err != nil { + return nil, err + } + return &repositorypb.RepositorySyncScheduleResponse{Schedule: repositoryScheduleToPB(value)}, nil +} + +func repositoryScheduleToPB(value *po.RepositorySyncSchedule) *repositorypb.RepositorySyncSchedule { + if value == nil { + return nil + } + return &repositorypb.RepositorySyncSchedule{ + Id: value.ID, RepositoryId: value.RepositoryID, ProjectId: value.ProjectID, + Cadence: scheduleCadenceToPB(value.Cadence), Timezone: value.Timezone, + DayOfWeek: int32(value.DayOfWeek), Hour: int32(value.Hour), Minute: int32(value.Minute), + Status: scheduleStatusToPB(value.Status), LastRunAt: optionalTimestamp(value.LastRunAt), + NextRunAt: timestamppb.New(value.NextRunAt), ConsecutiveFailures: int32(value.ConsecutiveFailures), + FailureThreshold: int32(value.FailureThreshold), LastErrorCode: value.LastErrorCode, + LastErrorMessage: value.LastErrorMessage, CreatedBy: value.CreatedBy, + CreatedAt: timestamppb.New(value.CreatedAt), UpdatedAt: timestamppb.New(value.UpdatedAt), + } +} + +func scheduleCadenceToPB(value string) repositorypb.RepositorySyncScheduleCadence { + if value == request.RepositoryScheduleCadenceDaily { + return repositorypb.RepositorySyncScheduleCadence_REPOSITORY_SYNC_SCHEDULE_CADENCE_DAILY + } + if value == request.RepositoryScheduleCadenceWeekly { + return repositorypb.RepositorySyncScheduleCadence_REPOSITORY_SYNC_SCHEDULE_CADENCE_WEEKLY + } + return repositorypb.RepositorySyncScheduleCadence_REPOSITORY_SYNC_SCHEDULE_CADENCE_UNSPECIFIED +} + +func scheduleStatusToPB(value string) repositorypb.RepositorySyncScheduleStatus { + switch value { + case request.RepositoryScheduleStatusEnabled: + return repositorypb.RepositorySyncScheduleStatus_REPOSITORY_SYNC_SCHEDULE_STATUS_ENABLED + case request.RepositoryScheduleStatusPaused: + return repositorypb.RepositorySyncScheduleStatus_REPOSITORY_SYNC_SCHEDULE_STATUS_PAUSED + case request.RepositoryScheduleStatusAutoPaused: + return repositorypb.RepositorySyncScheduleStatus_REPOSITORY_SYNC_SCHEDULE_STATUS_AUTO_PAUSED + default: + return repositorypb.RepositorySyncScheduleStatus_REPOSITORY_SYNC_SCHEDULE_STATUS_UNSPECIFIED + } +} diff --git a/manager/backend/services/footstone/presentation/repository_task_controller.go b/manager/backend/services/footstone/presentation/repository_task_controller.go new file mode 100644 index 0000000000000000000000000000000000000000..f4e1ca2474c7baaeada94ba170d51380a31b8ae0 --- /dev/null +++ b/manager/backend/services/footstone/presentation/repository_task_controller.go @@ -0,0 +1,292 @@ +package presentation + +import ( + "context" + "fmt" + "io" + "strconv" + "time" + + commonpb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/common" + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/timestamppb" + thttp "trpc.group/trpc-go/trpc-go/http" +) + +func (c *RepositoryController) DescribeRepositoryTask(ctx context.Context, + req *repositorypb.DescribeRepositoryTaskRequest, +) (*repositorypb.RepositoryTaskResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + value, err := c.application.DescribeRepositoryTask(c.ToContext(ctx), req.GetTaskId()) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryTaskResponse{Task: repositoryTaskToPB(value)}, nil +} + +func (c *RepositoryController) DescribeRepositoryTaskLog(ctx context.Context, + req *repositorypb.DescribeRepositoryTaskLogRequest, +) (*repositorypb.RepositoryTaskLogResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + value, err := c.application.DescribeRepositoryTaskLog(c.ToContext(ctx), req.GetTaskId()) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryTaskLogResponse{TaskLog: &repositorypb.RepositoryTaskLog{ + TaskId: value.TaskID, StorageNodeId: value.StorageNodeID, Sha256: value.SHA256, + CompressedBytes: value.CompressedBytes, UncompressedBytes: value.UncompressedBytes, + LineCount: value.LineCount, Summary: value.Summary, ErrorFingerprint: value.ErrorFingerprint, + ExpiresAt: timestamppb.New(value.ExpiresAt), CreatedAt: timestamppb.New(value.CreatedAt), + }}, nil +} + +func (c *RepositoryController) DownloadRepositoryTaskLog(ctx context.Context, + req *repositorypb.DownloadRepositoryTaskLogRequest, +) (*emptypb.Empty, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + value, err := c.application.DownloadRepositoryTaskLog(c.ToContext(ctx), req.GetTaskId()) + if err != nil { + return nil, err + } + defer value.Content.Close() + response := thttp.Head(ctx).Response + response.Header().Set("Content-Type", value.ContentType) + response.Header().Set("Content-Disposition", "attachment; filename="+value.FileName) + response.Header().Set("Content-Length", strconv.FormatInt(value.SizeBytes, 10)) + response.Header().Set("X-Content-SHA256", value.SHA256) + response.Header().Set("Cache-Control", "no-store") + if err = copyRepositoryTaskLog(response, value.Content); err != nil { + return nil, fmt.Errorf("stream repository task log: %w", err) + } + return &emptypb.Empty{}, nil +} + +func copyRepositoryTaskLog(destination io.Writer, source io.Reader) error { + _, err := io.Copy(destination, source) + return err +} + +func (c *RepositoryController) ListRepositoryTasks(ctx context.Context, + req *repositorypb.ListRepositoryTasksRequest, +) (*repositorypb.ListRepositoryTasksResponse, error) { + values, page, statistics, err := c.application.ListRepositoryTasks(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + response := &repositorypb.ListRepositoryTasksResponse{ + Tasks: make([]*repositorypb.RepositoryTask, 0, len(values)), + Statistics: &repositorypb.RepositoryTaskStatistics{ + PendingCount: statistics.PendingCount, RunningCount: statistics.RunningCount, + FailedCount: statistics.FailedCount, SuccessCount: statistics.SuccessCount, + }, + PageResponse: &commonpb.PageResponse{ + Count: page.Count, + PageSize: int64(page.PageSize), + PageNumber: int64(page.PageNumber), + PageCount: int64(page.PageCount), + }, + } + for index := range values { + response.Tasks = append(response.Tasks, repositoryTaskToPB(&values[index])) + } + return response, nil +} + +func (c *RepositoryController) CancelRepositoryTask(ctx context.Context, + req *repositorypb.CancelRepositoryTaskRequest, +) (*repositorypb.RepositoryTaskResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + value, err := c.application.CancelRepositoryTask(c.ToContext(ctx), req.GetTaskId()) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryTaskResponse{Task: repositoryTaskToPB(value)}, nil +} + +func (c *RepositoryController) RetryRepositoryTask(ctx context.Context, + req *repositorypb.RetryRepositoryTaskRequest, +) (*repositorypb.RepositoryTaskResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + value, err := c.application.RetryRepositoryTask(c.ToContext(ctx), req.GetTaskId()) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryTaskResponse{Task: repositoryTaskToPB(value)}, nil +} + +func (c *RepositoryController) ExportRepositoryTaskDiagnosis(ctx context.Context, + req *repositorypb.ExportRepositoryTaskDiagnosisRequest, +) (*repositorypb.RepositoryDiagnosisReportResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + value, err := c.application.ExportRepositoryTaskDiagnosis(c.ToContext(ctx), req.GetTaskId()) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryDiagnosisReportResponse{ + FileName: value.FileName, ContentType: value.ContentType, Sha256: value.SHA256, Content: value.Content, + }, nil +} + +func repositoryTaskToPB(value *po.RepositoryTask) *repositorypb.RepositoryTask { + if value == nil { + return nil + } + errorMessage, recommendedAction := repositoryErrorLocalization(value.ErrorCode) + result := &repositorypb.RepositoryTask{ + Id: value.ID, + RootTaskId: value.RootTaskID, + RetryOfTaskId: value.RetryOfTaskID, + RepositoryId: value.RepositoryID, + ProjectId: value.ProjectID, + RevisionId: value.RevisionID, + Type: taskTypeToPB(value.Type), + Status: taskStatusToPB(value.Status), + RequiresMutationLock: value.RequiresMutationLock, + Attempt: int32(value.Attempt), + MaxAttempts: int32(value.MaxAttempts), + RequestedBy: value.RequestedBy, + WorkerId: value.WorkerID, + FencingToken: value.FencingToken, + LeaseExpiresAt: optionalTimestamp(value.LeaseExpiresAt), + LastHeartbeatAt: optionalTimestamp(value.LastHeartbeatAt), + AvailableAt: timestamppb.New(value.AvailableAt), + StartedAt: optionalTimestamp(value.StartedAt), + FinishedAt: optionalTimestamp(value.FinishedAt), + CancelRequestedAt: optionalTimestamp(value.CancelRequestedAt), + ProgressPercent: value.ProgressPercent, + CurrentStep: value.CurrentStep, + ErrorCode: value.ErrorCode, + ErrorMessage: value.ErrorMessage, + StorageNodeId: value.StorageNodeID, + PreflightId: value.PreflightID, + CommitState: value.CommitState, + CommitToken: value.CommitToken, + PreparedRevisionId: value.PreparedRevisionID, + PreparedManifestSha256: value.PreparedManifestSHA256, + PreparedAt: optionalTimestamp(value.PreparedAt), + ActivatedAt: optionalTimestamp(value.ActivatedAt), + ConfirmedAt: optionalTimestamp(value.ConfirmedAt), + ResultObjectId: value.ResultObjectID, + ResultObjectSha256: value.ResultObjectSHA256, + RepositoryName: value.RepositoryName, + StorageNodeName: value.StorageNodeName, + Trigger: value.Trigger, + IdempotencyKey: stringValue(value.IdempotencyKey), + BandwidthLimitBytes: value.BandwidthLimitBytes, + MaxParallelDownloads: int32(value.MaxParallelDownloads), + ScopeType: value.ScopeType, + ScopeId: value.ScopeID, + ExecutorKind: value.ExecutorKind, + ErrorZhMessage: errorMessage, + ErrorZhRecommendedAction: recommendedAction, + CreatedAt: timestamppb.New(value.CreatedAt), + UpdatedAt: timestamppb.New(value.UpdatedAt), + } + for index := range value.Targets { + result.Targets = append(result.Targets, repositoryTaskTargetToPB(&value.Targets[index])) + } + for index := range value.ResultItems { + item := &value.ResultItems[index] + result.ResultItems = append(result.ResultItems, &repositorypb.RepositoryTaskResultItem{ + Id: item.ID, TaskId: item.TaskID, ItemKey: item.ItemKey, + PackageNevra: item.PackageNEVRA, Status: taskStatusToPB(item.Status), + ErrorCode: item.ErrorCode, Message: item.Message, ResultJson: string(item.ResultJSON), + StartedAt: optionalTimestamp(item.StartedAt), FinishedAt: optionalTimestamp(item.FinishedAt), + CreatedAt: timestamppb.New(item.CreatedAt), + }) + } + if diagnosis := value.StructuredDiagnosis(); diagnosis != nil { + result.Diagnosis = &repositorypb.RepositoryDiagnosis{ + FailureCategory: diagnosis.FailureCategory, + ErrorCode: diagnosis.ErrorCode, + FailedStep: diagnosis.FailedStep, + LikelyCause: diagnosis.LikelyCause, + Evidence: append([]string(nil), diagnosis.Evidence...), + RecommendedAction: diagnosis.RecommendedAction, + RuleId: diagnosis.RuleID, + GeneratedAt: timestamppb.New(diagnosis.GeneratedAt), + } + } + return result +} + +func stringValue(value *string) string { + if value == nil { + return "" + } + return *value +} + +func taskTypeToPB(value string) repositorypb.RepositoryTaskType { + switch value { + case request.RepositoryTaskTypeSync: + return repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_SYNC + case request.RepositoryTaskTypeUpload: + return repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_UPLOAD + case request.RepositoryTaskTypeDeletePackage: + return repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_DELETE_PACKAGE + case request.RepositoryTaskTypeRebuildMetadata: + return repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_REBUILD_METADATA + case request.RepositoryTaskTypeDeleteRepository: + return repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_DELETE_REPOSITORY + case request.RepositoryTaskTypePublish: + return repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_PUBLISH + case request.RepositoryTaskTypeValidate: + return repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_VALIDATE + case request.RepositoryTaskTypeDistribute: + return repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_DISTRIBUTE + case request.RepositoryTaskTypeMigrateRepository: + return repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_MIGRATE_REPOSITORY + case request.RepositoryTaskTypeNodeInstall: + return repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_NODE_INSTALL + case request.RepositoryTaskTypePackageInstall: + return repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_PACKAGE_INSTALL + case request.RepositoryTaskTypeInventoryImport: + return repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_INVENTORY_IMPORT + default: + return repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_UNSPECIFIED + } +} + +func taskStatusToPB(value string) repositorypb.RepositoryTaskStatus { + switch value { + case request.RepositoryTaskStatusPending: + return repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_PENDING + case request.RepositoryTaskStatusRunning: + return repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_RUNNING + case request.RepositoryTaskStatusCancelRequested: + return repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_CANCEL_REQUESTED + case request.RepositoryTaskStatusCanceled: + return repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_CANCELED + case request.RepositoryTaskStatusSuccess: + return repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_SUCCESS + case request.RepositoryTaskStatusFailed: + return repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_FAILED + case request.RepositoryTaskStatusPartialSuccess: + return repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_PARTIAL_SUCCESS + default: + return repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_UNSPECIFIED + } +} + +func optionalTimestamp(value *time.Time) *timestamppb.Timestamp { + if value == nil { + return nil + } + return timestamppb.New(*value) +} diff --git a/manager/backend/services/footstone/presentation/repository_task_controller_test.go b/manager/backend/services/footstone/presentation/repository_task_controller_test.go new file mode 100644 index 0000000000000000000000000000000000000000..5df05486ff978cd3fe115c589380f7d102dbe70e --- /dev/null +++ b/manager/backend/services/footstone/presentation/repository_task_controller_test.go @@ -0,0 +1,118 @@ +package presentation + +import ( + "encoding/json" + "errors" + "io" + "strings" + "testing" + "time" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +type repositoryTaskLogFailWriter struct{ err error } + +func (w repositoryTaskLogFailWriter) Write([]byte) (int, error) { return 0, w.err } + +func TestRepositoryTaskToPB(t *testing.T) { + now := time.Now().UTC().Truncate(time.Millisecond) + lease := now.Add(time.Minute) + value := &po.RepositoryTask{ + ID: "11111111-1111-1111-1111-111111111111", + RootTaskID: "11111111-1111-1111-1111-111111111111", + RepositoryID: "22222222-2222-2222-2222-222222222222", + ProjectID: 42, + Type: request.RepositoryTaskTypeUpload, + Trigger: request.RepositoryTaskTriggerSystem, + Status: request.RepositoryTaskStatusCancelRequested, + RequiresMutationLock: true, + Attempt: 2, + MaxAttempts: 3, + WorkerID: "worker-1", + FencingToken: 7, + LeaseExpiresAt: &lease, + AvailableAt: now, + ProgressPercent: 50, + CreatedAt: now, + UpdatedAt: now, + FailureCategory: "DEPENDENCY", + FailedStep: "resolve package dependencies", + LikelyCause: "a dependency is missing", + DiagnosisEvidenceJSON: []byte(`["error_code=DNF_VALIDATION_FAILED","message=nothing provides fixture"]`), + RecommendedAction: "publish the provider package", + ErrorCode: "DNF_DEPENDENCY_MISSING", + ErrorMessage: "nothing provides fixture", + DiagnosisRuleID: "DNF_DEPENDENCY_MISSING", + DiagnosisGeneratedAt: &now, + ResultItems: []po.RepositoryTaskResultItem{{ + ID: "33333333-3333-4333-8333-333333333333", TaskID: "11111111-1111-1111-1111-111111111111", + ItemKey: "fixture-0:1.0-1.noarch", PackageNEVRA: "fixture-0:1.0-1.noarch", + Status: request.RepositoryTaskStatusFailed, ErrorCode: "REVISION_BUILD_FAILED", + Message: "createrepo_c failed", ResultJSON: []byte(`{"deleted":false}`), + StartedAt: &now, FinishedAt: &now, CreatedAt: now, + }}, + } + got := repositoryTaskToPB(value) + if got.GetType() != repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_UPLOAD || + got.GetStatus() != repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_CANCEL_REQUESTED || + got.GetTrigger() != request.RepositoryTaskTriggerSystem { + t.Fatalf("repositoryTaskToPB() type/status = %s/%s", got.GetType(), got.GetStatus()) + } + if got.GetFencingToken() != 7 || got.GetLeaseExpiresAt() == nil || got.GetFinishedAt() != nil || + got.GetAvailableAt().AsTime() != now { + t.Fatalf("repositoryTaskToPB() = %#v", got) + } + if got.GetDiagnosis().GetRuleId() != "DNF_DEPENDENCY_MISSING" || + len(got.GetDiagnosis().GetEvidence()) != 2 || got.GetDiagnosis().GetGeneratedAt().AsTime() != now { + encoded, _ := json.Marshal(got.GetDiagnosis()) + t.Fatalf("repositoryTaskToPB() diagnosis = %s", encoded) + } + if got.GetErrorZhMessage() == "" || got.GetErrorZhRecommendedAction() == "" || + got.GetErrorMessage() != "nothing provides fixture" { + t.Fatalf("repositoryTaskToPB() localized error = %#v", got) + } + if len(got.GetResultItems()) != 1 || got.GetResultItems()[0].GetPackageNevra() != "fixture-0:1.0-1.noarch" || + got.GetResultItems()[0].GetStatus() != repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_FAILED || + got.GetResultItems()[0].GetErrorCode() != "REVISION_BUILD_FAILED" || + got.GetResultItems()[0].GetResultJson() != `{"deleted":false}` { + t.Fatalf("repositoryTaskToPB() result items = %#v", got.GetResultItems()) + } +} + +func TestRepositoryErrorLocalizationKeepsSuccessEmptyAndUnknownActionable(t *testing.T) { + if message, action := repositoryErrorLocalization(""); message != "" || action != "" { + t.Fatalf("empty error localization = %q/%q", message, action) + } + message, action := repositoryErrorLocalization("VENDOR_NEW_ERROR") + if message == "" || action == "" { + t.Fatalf("unknown error localization = %q/%q", message, action) + } +} + +func TestTaskEnumFallbacks(t *testing.T) { + if taskTypeToPB("UNKNOWN") != repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_UNSPECIFIED || + taskStatusToPB("UNKNOWN") != repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_UNSPECIFIED { + t.Fatal("unknown task enum did not map to UNSPECIFIED") + } +} + +func TestCopyRepositoryTaskLog(t *testing.T) { + var output strings.Builder + if err := copyRepositoryTaskLog(&output, strings.NewReader("compressed-log")); err != nil { + t.Fatal(err) + } + if output.String() != "compressed-log" { + t.Fatalf("copyRepositoryTaskLog() output = %q", output.String()) + } + wantErr := errors.New("client disconnected") + if err := copyRepositoryTaskLog(repositoryTaskLogFailWriter{err: wantErr}, + strings.NewReader("content")); !errors.Is(err, wantErr) { + t.Fatalf("copyRepositoryTaskLog() error = %v, want %v", err, wantErr) + } + if err := copyRepositoryTaskLog(io.Discard, strings.NewReader("")); err != nil { + t.Fatalf("copyRepositoryTaskLog() empty error = %v", err) + } +} diff --git a/manager/backend/services/footstone/presentation/repository_upstream_controller.go b/manager/backend/services/footstone/presentation/repository_upstream_controller.go new file mode 100644 index 0000000000000000000000000000000000000000..59dd180a607162cc49bd1f4bbe84b9a647196a84 --- /dev/null +++ b/manager/backend/services/footstone/presentation/repository_upstream_controller.go @@ -0,0 +1,114 @@ +package presentation + +import ( + "context" + "fmt" + + commonpb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/common" + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func (c *RepositoryController) CreateRepositoryUpstreamProfile(ctx context.Context, + req *repositorypb.CreateRepositoryUpstreamProfileRequest, +) (*repositorypb.RepositoryUpstreamProfileResponse, error) { + value, err := c.application.CreateRepositoryUpstreamProfile(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + pbValue, err := repositoryUpstreamProfileToPB(value) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryUpstreamProfileResponse{Profile: pbValue}, nil +} + +func (c *RepositoryController) UpdateRepositoryUpstreamProfile(ctx context.Context, + req *repositorypb.UpdateRepositoryUpstreamProfileRequest, +) (*repositorypb.RepositoryUpstreamProfileResponse, error) { + value, err := c.application.UpdateRepositoryUpstreamProfile(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + pbValue, err := repositoryUpstreamProfileToPB(value) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryUpstreamProfileResponse{Profile: pbValue}, nil +} + +func (c *RepositoryController) DescribeRepositoryUpstreamProfile(ctx context.Context, + req *repositorypb.DescribeRepositoryUpstreamProfileRequest, +) (*repositorypb.RepositoryUpstreamProfileResponse, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + value, err := c.application.DescribeRepositoryUpstreamProfile(c.ToContext(ctx), req.GetUpstreamProfileId()) + if err != nil { + return nil, err + } + pbValue, err := repositoryUpstreamProfileToPB(value) + if err != nil { + return nil, err + } + return &repositorypb.RepositoryUpstreamProfileResponse{Profile: pbValue}, nil +} + +func (c *RepositoryController) ListRepositoryUpstreamProfiles(ctx context.Context, + req *repositorypb.ListRepositoryUpstreamProfilesRequest, +) (*repositorypb.ListRepositoryUpstreamProfilesResponse, error) { + values, page, err := c.application.ListRepositoryUpstreamProfiles(c.ToContext(ctx), req) + if err != nil { + return nil, err + } + response := &repositorypb.ListRepositoryUpstreamProfilesResponse{ + Profiles: make([]*repositorypb.RepositoryUpstreamProfile, 0, len(values)), + PageResponse: &commonpb.PageResponse{Count: page.Count, PageSize: int64(page.PageSize), + PageNumber: int64(page.PageNumber), PageCount: int64(page.PageCount)}, + } + for index := range values { + pbValue, convertErr := repositoryUpstreamProfileToPB(&values[index]) + if convertErr != nil { + return nil, convertErr + } + response.Profiles = append(response.Profiles, pbValue) + } + return response, nil +} + +func (c *RepositoryController) DeleteRepositoryUpstreamProfile(ctx context.Context, + req *repositorypb.DeleteRepositoryUpstreamProfileRequest, +) (*emptypb.Empty, error) { + if req == nil { + return nil, fmt.Errorf("request is required") + } + if err := c.application.DeleteRepositoryUpstreamProfile(c.ToContext(ctx), req.GetUpstreamProfileId()); err != nil { + return nil, err + } + return &emptypb.Empty{}, nil +} + +func repositoryUpstreamProfileToPB(value *po.RepositoryUpstreamProfile) ( + *repositorypb.RepositoryUpstreamProfile, error, +) { + if value == nil { + return nil, fmt.Errorf("repository upstream profile is required") + } + versions, architectures, include, exclude, err := value.Collections() + if err != nil { + return nil, err + } + pbArchitectures := make([]repositorypb.RepositoryArchitecture, 0, len(architectures)) + for _, architecture := range architectures { + pbArchitectures = append(pbArchitectures, architectureToPB(architecture)) + } + return &repositorypb.RepositoryUpstreamProfile{ + Id: value.ID, ProjectId: value.ProjectID, Name: value.Name, Description: value.Description, + SystemProduct: value.SystemProduct, Channel: value.Channel, UrlTemplate: value.URLTemplate, + SupportedVersions: versions, SupportedArchitectures: pbArchitectures, + DefaultIncludePackages: include, DefaultExcludePackages: exclude, Enabled: value.Enabled, + CreatedBy: value.CreatedBy, CreatedAt: timestamppb.New(value.CreatedAt), UpdatedAt: timestamppb.New(value.UpdatedAt), + }, nil +} diff --git a/manager/backend/services/footstone/presentation/repository_upstream_controller_test.go b/manager/backend/services/footstone/presentation/repository_upstream_controller_test.go new file mode 100644 index 0000000000000000000000000000000000000000..c5e807e113f2175a896ff68aeb74f59cc23832e1 --- /dev/null +++ b/manager/backend/services/footstone/presentation/repository_upstream_controller_test.go @@ -0,0 +1,59 @@ +package presentation + +import ( + "testing" + "time" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +func TestRepositoryUpstreamProfileToPB(t *testing.T) { + now := time.Now().UTC().Truncate(time.Millisecond) + value := &po.RepositoryUpstreamProfile{ + ID: "11111111-1111-1111-1111-111111111111", + ProjectID: 42, + Name: "OpenCloudOS BaseOS", + SystemProduct: "OpenCloudOS", + Channel: "BaseOS", + URLTemplate: "https://mirrors.opencloudos.org/opencloudos/{version}/BaseOS/{architecture}/os/", + SupportedVersionsJSON: `["9.6"]`, + SupportedArchitecturesJSON: `["x86_64","aarch64"]`, + DefaultIncludePackagesJSON: `["bash"]`, + DefaultExcludePackagesJSON: `["*-debug"]`, + Enabled: true, + CreatedAt: now, + UpdatedAt: now, + } + + got, err := repositoryUpstreamProfileToPB(value) + if err != nil { + t.Fatal(err) + } + if got.GetName() != value.Name || got.GetUrlTemplate() != value.URLTemplate || + len(got.GetSupportedVersions()) != 1 || len(got.GetDefaultIncludePackages()) != 1 { + t.Fatalf("repositoryUpstreamProfileToPB() = %#v", got) + } + if got.GetSupportedArchitectures()[0] != repositorypb.RepositoryArchitecture_REPOSITORY_ARCHITECTURE_X86_64 || + got.GetSupportedArchitectures()[1] != repositorypb.RepositoryArchitecture_REPOSITORY_ARCHITECTURE_AARCH64 { + t.Fatalf("architecture mapping = %v", got.GetSupportedArchitectures()) + } + if got.GetCreatedAt().AsTime() != now || got.GetUpdatedAt().AsTime() != now { + t.Fatalf("timestamps = %s/%s", got.GetCreatedAt(), got.GetUpdatedAt()) + } +} + +func TestRepositoryUpstreamProfileToPBRejectsInvalidValue(t *testing.T) { + if _, err := repositoryUpstreamProfileToPB(nil); err == nil { + t.Fatal("repositoryUpstreamProfileToPB(nil) error = nil") + } + value := &po.RepositoryUpstreamProfile{ + SupportedVersionsJSON: "not-json", + SupportedArchitecturesJSON: "[]", + DefaultIncludePackagesJSON: "[]", + DefaultExcludePackagesJSON: "[]", + } + if _, err := repositoryUpstreamProfileToPB(value); err == nil { + t.Fatal("repositoryUpstreamProfileToPB(invalid JSON) error = nil") + } +} diff --git a/manager/backend/services/footstone/presentation/svc.go b/manager/backend/services/footstone/presentation/svc.go index e5af110a8ff01aa09e251b9f0faa84d6dce23a3a..76e15171770b232fc64e849edeb47d016b9325e7 100644 --- a/manager/backend/services/footstone/presentation/svc.go +++ b/manager/backend/services/footstone/presentation/svc.go @@ -11,6 +11,7 @@ import ( "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/wire" ftpb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/footstone" pb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/footstone" + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/application" "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" trpchandler "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/trpc" @@ -56,7 +57,9 @@ func (s *GatewayServer) Start() error { trpc.DefaultMaxFrameSize = 500 * 1024 * 1024 svr := trpc.NewServer() // 常规http、trpc服务 - InitGatewayController(s.appSvc, appconf, svr) + if err := InitGatewayController(s.appSvc, appconf, svr); err != nil { + return err + } s.server = svr s.wg.Add(1) go func() { @@ -103,7 +106,7 @@ func RegisterHttpReqHandler() { } // InitGatewayController create gateway presentation implement instance -func InitGatewayController(app application.Application, appconf *config.ApplicationConfig, s *server.Server) { +func InitGatewayController(app application.Application, appconf *config.ApplicationConfig, s *server.Server) error { thttp.RegisterNoProtocolServiceMux( s.Service("http.ocmanager.footstone.health"), health.NewMux("footstone", promhttp.Handler()), @@ -133,4 +136,10 @@ func InitGatewayController(app application.Application, appconf *config.Applicat pb.RegisterUserFavoriteService(s, NewUserFavoriteController(bcs)) pb.RegisterMetricsMetaService(s, NewMetricsMetaController(bcs)) pb.RegisterPermissionService(s, NewPermissionController(bcs)) + repositorypb.RegisterRepositoryServiceService(s, NewRepositoryController(bcs)) + if err := registerRepositoryNodeInternalService(s, app, + appconf.Application.Repository.InternalControl); err != nil { + return err + } + return registerRepositoryNodeBootstrapService(s, app, appconf.Application.Repository.NodeBootstrap) } diff --git a/manager/backend/services/footstone/repositoryworker/bootstrap/client.go b/manager/backend/services/footstone/repositoryworker/bootstrap/client.go new file mode 100644 index 0000000000000000000000000000000000000000..c0a6f9d7ff78d0c3e6198983ff4fd463d2be215c --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/bootstrap/client.go @@ -0,0 +1,295 @@ +package bootstrap + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/hex" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/google/uuid" + "trpc.group/trpc-go/trpc-go/client" + + repositorynodepb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repositorynode" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryNodePlatform" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" +) + +const maximumBootstrapTokenBytes = 512 + +type authorizationState struct { + AuthorizationID string `json:"authorization_id"` + EnrollmentID string `json:"enrollment_id"` + TMSAgentID uint64 `json:"tms_agent_id"` + Architecture string `json:"architecture"` + PrivateKeyPEM []byte `json:"private_key_pem"` + CSRPEM []byte `json:"csr_pem"` + CSRSHA256 string `json:"csr_sha256"` +} + +// EnsureCredentials enrolls an unconfigured node. The private key is created +// locally and is never included in an RPC request or response. +func EnsureCredentials(ctx context.Context, cfg config.RepositoryNodeClientConfig, + repositoryRoot string, +) (config.RepositoryNodeClientConfig, error) { + // An existing certificate remains usable while an in-place reinstall is + // authorizing its replacement. A newly delivered bootstrap token is the + // explicit signal to perform that replacement instead of returning early. + if credentialsUsable(cfg) { + if strings.TrimSpace(cfg.BootstrapTokenFile) == "" { + return cfg, nil + } + if _, statErr := os.Stat(cfg.BootstrapTokenFile); errors.Is(statErr, os.ErrNotExist) { + return cfg, nil + } else if statErr != nil { + return cfg, fmt.Errorf("inspect repository node bootstrap token: %w", statErr) + } + } + if err := validateBootstrapConfig(cfg, repositoryRoot); err != nil { + return cfg, err + } + tokenBytes, err := os.ReadFile(cfg.BootstrapTokenFile) + if err != nil { + return cfg, fmt.Errorf("read repository node bootstrap token: %w", err) + } + if len(tokenBytes) > maximumBootstrapTokenBytes { + return cfg, errors.New("repository node bootstrap token file is too large") + } + token := strings.TrimSpace(string(tokenBytes)) + if len(token) < 32 { + return cfg, errors.New("repository node bootstrap token is invalid") + } + statePath := filepath.Join(filepath.Dir(cfg.ClientKey), "bootstrap-authorization.json") + state, err := loadOrCreateAuthorizationState(statePath, cfg) + if err != nil { + return cfg, err + } + proxy := repositorynodepb.NewRepositoryNodeBootstrapServiceClientProxy( + client.WithTarget(cfg.BootstrapURL), + client.WithTLS("", "", bootstrapServerCA(cfg), bootstrapServerName(cfg)), + client.WithTimeout(time.Duration(cfg.RPCSeconds)*time.Second)) + response, err := proxy.BootstrapNode(ctx, &repositorynodepb.BootstrapNodeRequest{ + EnrollmentId: cfg.EnrollmentID, InstallTaskId: cfg.InstallTaskID, TmsAgentId: cfg.TMSAgentID, + BootstrapToken: token, CsrPem: state.CSRPEM, AuthorizationId: state.AuthorizationID, + Architecture: state.Architecture, + }) + if err != nil { + return cfg, fmt.Errorf("bootstrap repository node: %w", err) + } + if response.GetStorageNodeId() != cfg.StorageNodeID { + return cfg, errors.New("repository node bootstrap returned a different node identity") + } + if err = verifyIssuedCredentials(state.PrivateKeyPEM, response.GetCertificatePem(), response.GetCaCertificatePem(), + cfg.StorageNodeID); err != nil { + return cfg, err + } + if err = atomicWrite(cfg.ClientKey, state.PrivateKeyPEM, 0600); err != nil { + return cfg, err + } + if err = atomicWrite(cfg.ClientCert, response.GetCertificatePem(), 0644); err != nil { + _ = os.Remove(cfg.ClientKey) + return cfg, err + } + clientCAPath := filepath.Join(filepath.Dir(cfg.ClientCert), "client-ca.crt") + if err = atomicWrite(clientCAPath, response.GetCaCertificatePem(), 0644); err != nil { + return cfg, err + } + if err = os.Remove(cfg.BootstrapTokenFile); err != nil && !errors.Is(err, os.ErrNotExist) { + return cfg, fmt.Errorf("remove consumed repository node bootstrap token: %w", err) + } + if err = os.Remove(statePath); err != nil && !errors.Is(err, os.ErrNotExist) { + return cfg, fmt.Errorf("remove repository node bootstrap authorization state: %w", err) + } + return cfg, nil +} + +func loadOrCreateAuthorizationState(statePath string, cfg config.RepositoryNodeClientConfig) ( + *authorizationState, error, +) { + content, err := os.ReadFile(statePath) + if err == nil { + value := new(authorizationState) + if err = json.Unmarshal(content, value); err != nil { + return nil, fmt.Errorf("decode repository node bootstrap authorization state: %w", err) + } + if value.EnrollmentID != cfg.EnrollmentID { + return createAuthorizationState(statePath, cfg) + } + if value.TMSAgentID != cfg.TMSAgentID || uuid.Validate(value.AuthorizationID) != nil || + len(value.PrivateKeyPEM) == 0 || len(value.CSRPEM) == 0 { + return nil, errors.New("repository node bootstrap authorization state does not match enrollment") + } + digest := sha256.Sum256(value.CSRPEM) + if subtleDigest := hex.EncodeToString(digest[:]); subtleDigest != value.CSRSHA256 { + return nil, errors.New("repository node bootstrap authorization state CSR digest is invalid") + } + return value, nil + } + if !errors.Is(err, os.ErrNotExist) { + return nil, fmt.Errorf("read repository node bootstrap authorization state: %w", err) + } + return createAuthorizationState(statePath, cfg) +} + +func createAuthorizationState(statePath string, cfg config.RepositoryNodeClientConfig) ( + *authorizationState, error, +) { + architecture, err := repositoryNodePlatform.NormalizeArchitecture(runtime.GOARCH) + if err != nil { + return nil, fmt.Errorf("normalize repository node architecture: %w", err) + } + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, fmt.Errorf("generate repository node private key: %w", err) + } + csrDER, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ + Subject: pkix.Name{CommonName: "repository-node:" + cfg.StorageNodeID, Organization: []string{"OCManager"}}, + }, privateKey) + if err != nil { + return nil, fmt.Errorf("create repository node CSR: %w", err) + } + keyDER, err := x509.MarshalPKCS8PrivateKey(privateKey) + if err != nil { + return nil, fmt.Errorf("encode repository node private key: %w", err) + } + csrPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrDER}) + digest := sha256.Sum256(csrPEM) + value := &authorizationState{AuthorizationID: uuid.NewString(), EnrollmentID: cfg.EnrollmentID, + TMSAgentID: cfg.TMSAgentID, Architecture: architecture, + PrivateKeyPEM: pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}), + CSRPEM: csrPEM, CSRSHA256: hex.EncodeToString(digest[:])} + encoded, err := json.Marshal(value) + if err != nil { + return nil, err + } + if err = atomicWrite(statePath, encoded, 0600); err != nil { + return nil, fmt.Errorf("persist repository node bootstrap authorization state: %w", err) + } + return value, nil +} + +func credentialsUsable(cfg config.RepositoryNodeClientConfig) bool { + if _, err := tls.LoadX509KeyPair(cfg.ClientCert, cfg.ClientKey); err != nil { + return false + } + return true +} + +func validateBootstrapConfig(cfg config.RepositoryNodeClientConfig, repositoryRoot string) error { + for field, value := range map[string]string{"storage_node_id": cfg.StorageNodeID, + "enrollment_id": cfg.EnrollmentID, "install_task_id": cfg.InstallTaskID} { + if uuid.Validate(strings.TrimSpace(value)) != nil { + return fmt.Errorf("repository node bootstrap %s must be a UUID", field) + } + } + if strings.TrimSpace(cfg.BootstrapURL) == "" || strings.TrimSpace(bootstrapServerName(cfg)) == "" || + strings.TrimSpace(bootstrapServerCA(cfg)) == "" || strings.TrimSpace(cfg.BootstrapTokenFile) == "" { + return errors.New("repository node bootstrap configuration is incomplete") + } + for label, filePath := range map[string]string{"bootstrap_token_file": cfg.BootstrapTokenFile, + "bootstrap_server_ca": bootstrapServerCA(cfg), "server_ca": cfg.ServerCA, + "client_cert": cfg.ClientCert, "client_key": cfg.ClientKey} { + if !filepath.IsAbs(filePath) { + return fmt.Errorf("repository node %s must be an absolute path", label) + } + if withinRoot(repositoryRoot, filePath) { + return fmt.Errorf("repository node %s must be outside repository-data", label) + } + } + return nil +} + +func bootstrapServerCA(cfg config.RepositoryNodeClientConfig) string { + if value := strings.TrimSpace(cfg.BootstrapServerCA); value != "" { + return value + } + return cfg.ServerCA +} + +func bootstrapServerName(cfg config.RepositoryNodeClientConfig) string { + if value := strings.TrimSpace(cfg.BootstrapServerName); value != "" { + return value + } + return cfg.ServerName +} + +func verifyIssuedCredentials(keyPEM, certificatePEM, caPEM []byte, nodeID string) error { + if _, err := tls.X509KeyPair(certificatePEM, keyPEM); err != nil { + return fmt.Errorf("repository node issued certificate does not match local private key: %w", err) + } + block, _ := pem.Decode(certificatePEM) + if block == nil { + return errors.New("repository node issued certificate is invalid") + } + certificate, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return err + } + roots := x509.NewCertPool() + if !roots.AppendCertsFromPEM(caPEM) { + return errors.New("repository node bootstrap CA certificate is invalid") + } + if _, err = certificate.Verify(x509.VerifyOptions{Roots: roots, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}}); err != nil { + return fmt.Errorf("verify repository node issued certificate: %w", err) + } + expectedURI := "urn:ocmanager:repository-node:" + nodeID + for _, identity := range certificate.URIs { + if identity.String() == expectedURI { + return nil + } + } + return errors.New("repository node issued certificate does not contain the allocated node identity") +} + +func atomicWrite(filePath string, content []byte, mode os.FileMode) error { + if err := os.MkdirAll(filepath.Dir(filePath), 0700); err != nil { + return err + } + temporary, err := os.CreateTemp(filepath.Dir(filePath), ".repository-node-credential-*") + if err != nil { + return err + } + temporaryPath := temporary.Name() + defer os.Remove(temporaryPath) + if err = temporary.Chmod(mode); err == nil { + _, err = temporary.Write(content) + } + if err == nil { + err = temporary.Sync() + } + closeErr := temporary.Close() + if err == nil { + err = closeErr + } + if err != nil { + return err + } + if err = os.Rename(temporaryPath, filePath); err != nil { + return fmt.Errorf("publish repository node credential: %w", err) + } + return nil +} + +func withinRoot(root, filePath string) bool { + root, rootErr := filepath.Abs(root) + filePath, pathErr := filepath.Abs(filePath) + if rootErr != nil || pathErr != nil { + return false + } + relative, err := filepath.Rel(root, filePath) + return err == nil && relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator)) +} diff --git a/manager/backend/services/footstone/repositoryworker/bootstrap/client_test.go b/manager/backend/services/footstone/repositoryworker/bootstrap/client_test.go new file mode 100644 index 0000000000000000000000000000000000000000..95f859842bdecb5de61fc5549dafee54d2b8785f --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/bootstrap/client_test.go @@ -0,0 +1,152 @@ +package bootstrap + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "net/url" + "os" + "path/filepath" + "testing" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" +) + +const bootstrapTestNodeID = "33333333-3333-4333-8333-333333333333" + +func TestVerifyIssuedCredentialsRequiresLocalKeyAndNodeIdentity(t *testing.T) { + keyPEM, certificatePEM, caPEM := bootstrapTestCredentials(t, bootstrapTestNodeID) + if err := verifyIssuedCredentials(keyPEM, certificatePEM, caPEM, bootstrapTestNodeID); err != nil { + t.Fatalf("verifyIssuedCredentials() error = %v", err) + } + if err := verifyIssuedCredentials(keyPEM, certificatePEM, caPEM, + "44444444-4444-4444-8444-444444444444"); err == nil { + t.Fatal("verifyIssuedCredentials() accepted a different allocated node identity") + } + otherKeyPEM, _, _ := bootstrapTestCredentials(t, bootstrapTestNodeID) + if err := verifyIssuedCredentials(otherKeyPEM, certificatePEM, caPEM, bootstrapTestNodeID); err == nil { + t.Fatal("verifyIssuedCredentials() accepted a certificate for another private key") + } +} + +func TestValidateBootstrapConfigKeepsSecretsOutsideRepositoryData(t *testing.T) { + root := filepath.Join(t.TempDir(), "repository-data") + cfg := config.RepositoryNodeClientConfig{ + StorageNodeID: bootstrapTestNodeID, + EnrollmentID: "55555555-5555-4555-8555-555555555555", + InstallTaskID: "66666666-6666-4666-8666-666666666666", + BootstrapURL: "ip://manager:19444", + ServerName: "manager", + ServerCA: filepath.Join(root, "pki", "server-ca.crt"), + ClientCert: filepath.Join(t.TempDir(), "node.crt"), + ClientKey: filepath.Join(t.TempDir(), "node.key"), + BootstrapTokenFile: filepath.Join(t.TempDir(), "bootstrap.token"), + } + if err := validateBootstrapConfig(cfg, root); err == nil { + t.Fatal("validateBootstrapConfig() accepted CA material inside repository-data") + } + cfg.ServerCA = filepath.Join(t.TempDir(), "server-ca.crt") + if err := validateBootstrapConfig(cfg, root); err != nil { + t.Fatalf("validateBootstrapConfig() error = %v", err) + } +} + +func TestAtomicWritePublishesCompleteCredential(t *testing.T) { + path := filepath.Join(t.TempDir(), "pki", "node.key") + if err := atomicWrite(path, []byte("private"), 0600); err != nil { + t.Fatal(err) + } + content, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if string(content) != "private" { + t.Fatalf("credential content = %q", content) + } + if info, err := os.Stat(path); err != nil || info.Mode().Perm() != 0600 { + t.Fatalf("credential mode = %v, err = %v", info.Mode().Perm(), err) + } +} + +func TestAuthorizationStatePersistsIdentityForExactRetry(t *testing.T) { + statePath := filepath.Join(t.TempDir(), "pki", "bootstrap-authorization.json") + cfg := config.RepositoryNodeClientConfig{StorageNodeID: bootstrapTestNodeID, + EnrollmentID: "55555555-5555-4555-8555-555555555555", + InstallTaskID: "66666666-6666-4666-8666-666666666666", TMSAgentID: 991} + first, err := loadOrCreateAuthorizationState(statePath, cfg) + if err != nil { + t.Fatal(err) + } + second, err := loadOrCreateAuthorizationState(statePath, cfg) + if err != nil { + t.Fatal(err) + } + if first.AuthorizationID != second.AuthorizationID || first.CSRSHA256 != second.CSRSHA256 || + !bytes.Equal(first.CSRPEM, second.CSRPEM) || !bytes.Equal(first.PrivateKeyPEM, second.PrivateKeyPEM) { + t.Fatalf("authorization identity changed across retry: first=%#v second=%#v", first, second) + } + other := cfg + other.EnrollmentID = "77777777-7777-4777-8777-777777777777" + third, err := loadOrCreateAuthorizationState(statePath, other) + if err != nil { + t.Fatal(err) + } + if third.EnrollmentID != other.EnrollmentID || third.AuthorizationID == first.AuthorizationID || + third.CSRSHA256 == first.CSRSHA256 { + t.Fatal("a different enrollment did not receive a fresh authorization identity") + } + differentAgent := other + differentAgent.TMSAgentID++ + if _, err = loadOrCreateAuthorizationState(statePath, differentAgent); err == nil { + t.Fatal("authorization state was reused by a different TMS agent") + } +} + +func bootstrapTestCredentials(t *testing.T, nodeID string) ([]byte, []byte, []byte) { + t.Helper() + now := time.Now().UTC() + caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + caTemplate := &x509.Certificate{SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "ca"}, + NotBefore: now.Add(-time.Hour), NotAfter: now.Add(time.Hour), IsCA: true, + BasicConstraintsValid: true, KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature} + caDER, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey) + if err != nil { + t.Fatal(err) + } + ca, err := x509.ParseCertificate(caDER) + if err != nil { + t.Fatal(err) + } + nodeKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + identity, err := url.Parse("urn:ocmanager:repository-node:" + nodeID) + if err != nil { + t.Fatal(err) + } + certificateTemplate := &x509.Certificate{SerialNumber: big.NewInt(2), Subject: pkix.Name{CommonName: nodeID}, + NotBefore: now.Add(-time.Minute), NotAfter: now.Add(30 * time.Minute), + KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + URIs: []*url.URL{identity}} + certificateDER, err := x509.CreateCertificate(rand.Reader, certificateTemplate, ca, &nodeKey.PublicKey, caKey) + if err != nil { + t.Fatal(err) + } + keyDER, err := x509.MarshalPKCS8PrivateKey(nodeKey) + if err != nil { + t.Fatal(err) + } + return pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}), + pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certificateDER}), + pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caDER}) +} diff --git a/manager/backend/services/footstone/repositoryworker/command_log.go b/manager/backend/services/footstone/repositoryworker/command_log.go new file mode 100644 index 0000000000000000000000000000000000000000..464c08f224a197783d46941d4cc1ba7aede8741f --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/command_log.go @@ -0,0 +1,96 @@ +package repositoryworker + +import ( + "bufio" + "bytes" + "context" + "fmt" + "io" + "os/exec" + "sync" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/tasklog" +) + +const commandOutputTailBytes = 64 << 10 + +func runLoggedCommand(ctx context.Context, step string, command *exec.Cmd) ([]byte, error) { + stdout, err := command.StdoutPipe() + if err != nil { + return nil, err + } + stderr, err := command.StderrPipe() + if err != nil { + return nil, err + } + if err = tasklog.Event(ctx, "INFO", step, "command started", map[string]string{ + "executable": command.Path, + }); err != nil { + return nil, err + } + if err = command.Start(); err != nil { + return nil, err + } + tail := newBoundedTail(commandOutputTailBytes) + var wg sync.WaitGroup + errorsChannel := make(chan error, 2) + for stream, reader := range map[string]io.Reader{"stdout": stdout, "stderr": stderr} { + wg.Add(1) + go func(stream string, reader io.Reader) { + defer wg.Done() + scanner := bufio.NewScanner(reader) + scanner.Buffer(make([]byte, 64<<10), 1<<20) + for scanner.Scan() { + line := scanner.Text() + tail.WriteLine(stream + ": " + line) + if eventErr := tasklog.Event(ctx, "INFO", step, line, map[string]string{"stream": stream}); eventErr != nil { + errorsChannel <- eventErr + return + } + } + if scanErr := scanner.Err(); scanErr != nil { + errorsChannel <- scanErr + } + }(stream, reader) + } + wg.Wait() + close(errorsChannel) + for streamErr := range errorsChannel { + if streamErr != nil { + _ = command.Process.Kill() + _ = command.Wait() + return tail.Bytes(), fmt.Errorf("stream command output: %w", streamErr) + } + } + waitErr := command.Wait() + level, message := "INFO", "command completed" + if waitErr != nil { + level, message = "ERROR", "command failed" + } + _ = tasklog.Event(ctx, level, step, message, map[string]string{"output_tail": string(tail.Bytes())}) + return tail.Bytes(), waitErr +} + +type boundedTail struct { + mu sync.Mutex + maximum int + buffer []byte +} + +func newBoundedTail(maximum int) *boundedTail { return &boundedTail{maximum: maximum} } + +func (b *boundedTail) WriteLine(value string) { + b.mu.Lock() + defer b.mu.Unlock() + b.buffer = append(b.buffer, value...) + b.buffer = append(b.buffer, '\n') + if len(b.buffer) > b.maximum { + b.buffer = append([]byte(nil), b.buffer[len(b.buffer)-b.maximum:]...) + } +} + +func (b *boundedTail) Bytes() []byte { + b.mu.Lock() + defer b.mu.Unlock() + return bytes.TrimSpace(append([]byte(nil), b.buffer...)) +} diff --git a/manager/backend/services/footstone/repositoryworker/command_log_test.go b/manager/backend/services/footstone/repositoryworker/command_log_test.go new file mode 100644 index 0000000000000000000000000000000000000000..11bdcf00bdc2033d3ccbeebb737863ccb48ec727 --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/command_log_test.go @@ -0,0 +1,40 @@ +//go:build linux + +package repositoryworker + +import ( + "context" + "os/exec" + "strings" + "testing" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/tasklog" +) + +func TestRunLoggedCommandStreamsOutputAndKeepsBoundedTail(t *testing.T) { + store, err := storage.New(config.RepositoryConfig{Root: t.TempDir(), MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + writer, err := tasklog.New(store, loggingExecutorTaskID) + if err != nil { + t.Fatal(err) + } + ctx := tasklog.WithWriter(context.Background(), writer) + command := exec.CommandContext(ctx, "sh", "-c", + `i=0; while [ $i -lt 2000 ]; do printf 'line-%04d %04096d\n' "$i" 0; i=$((i+1)); done; printf 'password=hunter2\n' >&2; exit 7`) + output, commandErr := runLoggedCommand(ctx, "dnf-sync", command) + if commandErr == nil || len(output) > commandOutputTailBytes || !strings.Contains(string(output), "password=hunter2") { + t.Fatalf("command output/error = %d/%v", len(output), commandErr) + } + metadata, err := writer.Close("FAILED", "COMMAND_FAILED", string(output)) + if err != nil { + t.Fatal(err) + } + if metadata.UncompressedBytes < 8<<20 || metadata.CompressedBytes >= metadata.UncompressedBytes || + strings.Contains(metadata.Summary, "hunter2") { + t.Fatalf("streamed metadata = %#v", metadata) + } +} diff --git a/manager/backend/services/footstone/repositoryworker/controlplane/client.go b/manager/backend/services/footstone/repositoryworker/controlplane/client.go new file mode 100644 index 0000000000000000000000000000000000000000..fdc4e3e997165863081fec924cdaf72ee61286df --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/controlplane/client.go @@ -0,0 +1,348 @@ +package controlplane + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "net" + "net/url" + "os" + "path/filepath" + "strings" + "time" + + "github.com/google/uuid" + "google.golang.org/protobuf/types/known/timestamppb" + "trpc.group/trpc-go/trpc-go/client" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + repositorynodepb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repositorynode" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" +) + +type NodeHeartbeat struct { + Hostname string + IPAddress string + Architecture string + OSName string + OSVersion string + AgentVersion string + FilesystemType string + CapacityBytes int64 + FreeBytes int64 + FreeInodes int64 + ToolVersions map[string]string + ErrorCode string + ErrorMessage string + RepositoryUsages []RepositoryUsage + RepositoryUsageSnapshot bool + MetadataSigningKeys []MetadataSigningKey + MetadataKeySnapshot bool +} + +type MetadataSigningKey struct { + KeyRef string + Fingerprint string + PublicKeyName string + PublicKeyURL string + Status string + KeyCreatedAt *time.Time + ObservedAt time.Time +} + +type RepositoryUsage struct { + RepositoryID string + LogicalBytes int64 + PhysicalBytes int64 + PublishStatus string + ObservedAt time.Time +} + +type TaskHeartbeat struct { + TaskID string + WorkerID string + FencingToken uint64 + LeaseSeconds int32 + ProgressPercent int32 + CurrentStep string +} + +type TaskFailure struct { + TaskID string + WorkerID string + FencingToken uint64 + Status repositorypb.RepositoryTaskStatus + ErrorCode string + ErrorMessage string + ResultObjectID string + ResultObjectSHA256 string + QuarantineCount int64 +} + +type PreparedRevision struct { + TaskID string + WorkerID string + FencingToken uint64 + RevisionID string + ManifestSHA256 string + ResultObjectID string + ResultObjectSHA256 string + PackageCount int64 + TotalBytes int64 + CompletionStatus repositorypb.RepositoryTaskStatus + ErrorCode string + ErrorMessage string +} + +type ConfirmedRevision struct { + TaskID string + WorkerID string + FencingToken uint64 + CommitToken string +} + +type DistributionExecution struct { + TaskID string + WorkerID string + FencingToken uint64 +} + +type DistributionResult struct { + DistributionID string + TMSTaskID string + Status repositorypb.RepositoryTaskStatus + ErrorCode string + ErrorMessage string +} + +type Plane interface { + HeartbeatNode(context.Context, NodeHeartbeat) (*repositorynodepb.HeartbeatNodeResponse, error) + ClaimTask(context.Context, string, int32) (*repositorynodepb.RepositoryTaskSpec, error) + HeartbeatTask(context.Context, TaskHeartbeat) (*repositorynodepb.HeartbeatTaskResponse, error) + ReportTaskFailure(context.Context, TaskFailure) error + ExecuteDistributionTask(context.Context, DistributionExecution) (DistributionResult, error) + PrepareTaskCommit(context.Context, PreparedRevision) (*repositorynodepb.PrepareTaskCommitResponse, error) + ConfirmTaskCommit(context.Context, ConfirmedRevision) error +} + +type Client struct { + nodeID string + proxy repositorynodepb.RepositoryNodeInternalServiceClientProxy +} + +func New(cfg config.RepositoryNodeClientConfig, repositoryRoot string) (*Client, error) { + if cfg.HeartbeatSeconds <= 0 { + cfg.HeartbeatSeconds = 10 + } + if cfg.RPCSeconds <= 0 { + cfg.RPCSeconds = 10 + } + if err := validateConfig(cfg, repositoryRoot); err != nil { + return nil, err + } + return &Client{nodeID: cfg.StorageNodeID, proxy: repositorynodepb.NewRepositoryNodeInternalServiceClientProxy( + client.WithTarget(cfg.ControlTarget), + client.WithTLS(cfg.ClientCert, cfg.ClientKey, cfg.ServerCA, cfg.ServerName), + client.WithTimeout(time.Duration(cfg.RPCSeconds)*time.Second), + )}, nil +} + +func validateConfig(cfg config.RepositoryNodeClientConfig, repositoryRoot string) error { + if uuid.Validate(cfg.StorageNodeID) != nil { + return fmt.Errorf("repository node client requires a valid storage_node_id") + } + if err := validateControlTarget(cfg.ControlTarget); err != nil { + return err + } + if cfg.HeartbeatSeconds <= 0 || cfg.RPCSeconds <= 0 { + return fmt.Errorf("repository node heartbeat and RPC timeouts must be positive") + } + if cfg.ServerName = strings.TrimSpace(cfg.ServerName); cfg.ServerName == "" || hasControl(cfg.ServerName) { + return fmt.Errorf("repository node server_name is required and must not contain control characters") + } + for label, path := range map[string]string{ + "client_cert": cfg.ClientCert, "client_key": cfg.ClientKey, "server_ca": cfg.ServerCA, + } { + if !filepath.IsAbs(path) { + return fmt.Errorf("repository node %s must be an absolute path", label) + } + if withinRoot(repositoryRoot, path) { + return fmt.Errorf("repository node %s must be outside repository-data", label) + } + info, err := os.Stat(path) + if err != nil { + return fmt.Errorf("stat repository node %s: %w", label, err) + } + if !info.Mode().IsRegular() { + return fmt.Errorf("repository node %s is not a regular file", label) + } + } + if _, err := tls.LoadX509KeyPair(cfg.ClientCert, cfg.ClientKey); err != nil { + return fmt.Errorf("load repository node client certificate: %w", err) + } + caPEM, err := os.ReadFile(cfg.ServerCA) + if err != nil { + return fmt.Errorf("read repository node server CA: %w", err) + } + pool := x509.NewCertPool() + if !pool.AppendCertsFromPEM(caPEM) { + return fmt.Errorf("repository node server_ca contains no certificate") + } + return nil +} + +func validateControlTarget(value string) error { + parsed, err := url.Parse(strings.TrimSpace(value)) + if err != nil || parsed.Scheme != "ip" || parsed.Host == "" || parsed.User != nil || parsed.Path != "" || + parsed.RawQuery != "" || parsed.Fragment != "" { + return fmt.Errorf("repository node control_target must use ip://host:port without credentials or path") + } + if _, _, err = net.SplitHostPort(parsed.Host); err != nil { + return fmt.Errorf("repository node control_target must include a valid host and port: %w", err) + } + return nil +} + +func withinRoot(root, path string) bool { + if strings.TrimSpace(root) == "" { + return false + } + absRoot, err := filepath.Abs(root) + if err != nil { + return false + } + absPath, err := filepath.Abs(path) + if err != nil { + return false + } + relative, err := filepath.Rel(absRoot, absPath) + return err == nil && relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator)) +} + +func hasControl(value string) bool { + return strings.ContainsAny(value, "\r\n\t") +} + +func (c *Client) HeartbeatNode(ctx context.Context, input NodeHeartbeat) ( + *repositorynodepb.HeartbeatNodeResponse, error, +) { + usages := make([]*repositorynodepb.RepositoryUsage, 0, len(input.RepositoryUsages)) + for _, usage := range input.RepositoryUsages { + observedAt := usage.ObservedAt + if observedAt.IsZero() { + observedAt = time.Now().UTC() + } + usages = append(usages, &repositorynodepb.RepositoryUsage{RepositoryId: usage.RepositoryID, + LogicalBytes: usage.LogicalBytes, PhysicalBytes: usage.PhysicalBytes, + PublishStatus: usage.PublishStatus, ObservedAt: timestamppb.New(observedAt)}) + } + keys := make([]*repositorynodepb.MetadataSigningKeyInventory, 0, len(input.MetadataSigningKeys)) + for _, key := range input.MetadataSigningKeys { + observedAt := key.ObservedAt + if observedAt.IsZero() { + observedAt = time.Now().UTC() + } + value := &repositorynodepb.MetadataSigningKeyInventory{KeyRef: key.KeyRef, + Fingerprint: key.Fingerprint, PublicKeyName: key.PublicKeyName, + PublicKeyUrl: key.PublicKeyURL, Status: key.Status, ObservedAt: timestamppb.New(observedAt)} + if key.KeyCreatedAt != nil { + value.KeyCreatedAt = timestamppb.New(*key.KeyCreatedAt) + } + keys = append(keys, value) + } + return c.proxy.HeartbeatNode(ctx, &repositorynodepb.HeartbeatNodeRequest{ + StorageNodeId: c.nodeID, FilesystemType: input.FilesystemType, + CapacityBytes: input.CapacityBytes, FreeBytes: input.FreeBytes, FreeInodes: input.FreeInodes, + ToolVersions: input.ToolVersions, ObservedAt: timestamppb.Now(), + ErrorCode: input.ErrorCode, ErrorMessage: input.ErrorMessage, + Hostname: input.Hostname, IpAddress: input.IPAddress, Architecture: input.Architecture, + OsName: input.OSName, OsVersion: input.OSVersion, AgentVersion: input.AgentVersion, + RepositoryUsages: usages, RepositoryUsageSnapshot: input.RepositoryUsageSnapshot, + MetadataSigningKeys: keys, MetadataSigningKeySnapshot: input.MetadataKeySnapshot, + }) +} + +func (c *Client) RenewNodeCertificate(ctx context.Context, csrPEM []byte) ( + *repositorynodepb.NodeCertificateResponse, error, +) { + return c.proxy.RenewNodeCertificate(ctx, &repositorynodepb.RenewNodeCertificateRequest{ + StorageNodeId: c.nodeID, CsrPem: csrPEM, + }) +} + +func (c *Client) ClaimTask(ctx context.Context, workerID string, leaseSeconds int32) ( + *repositorynodepb.RepositoryTaskSpec, error, +) { + response, err := c.proxy.ClaimTask(ctx, &repositorynodepb.ClaimTaskRequest{ + StorageNodeId: c.nodeID, WorkerId: workerID, LeaseSeconds: leaseSeconds, + }) + if err != nil { + return nil, err + } + return response.GetTask(), nil +} + +func (c *Client) HeartbeatTask(ctx context.Context, input TaskHeartbeat) ( + *repositorynodepb.HeartbeatTaskResponse, error, +) { + return c.proxy.HeartbeatTask(ctx, &repositorynodepb.HeartbeatTaskRequest{ + TaskId: input.TaskID, WorkerId: input.WorkerID, FencingToken: input.FencingToken, + LeaseSeconds: input.LeaseSeconds, ProgressPercent: input.ProgressPercent, + CurrentStep: input.CurrentStep, StorageNodeId: c.nodeID, + }) +} + +func (c *Client) ReportTaskFailure(ctx context.Context, input TaskFailure) error { + if input.Status != repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_FAILED && + input.Status != repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_CANCELED { + return fmt.Errorf("repository node failure status must be FAILED or CANCELED") + } + _, err := c.proxy.ReportTaskFailure(ctx, &repositorynodepb.ReportTaskFailureRequest{ + TaskId: input.TaskID, WorkerId: input.WorkerID, FencingToken: input.FencingToken, + StorageNodeId: c.nodeID, Status: input.Status, + ErrorCode: input.ErrorCode, ErrorMessage: input.ErrorMessage, + ResultObjectId: input.ResultObjectID, ResultObjectSha256: input.ResultObjectSHA256, + QuarantineCount: input.QuarantineCount, + }) + return err +} + +func (c *Client) ExecuteDistributionTask(ctx context.Context, input DistributionExecution) ( + DistributionResult, error, +) { + response, err := c.proxy.ExecuteDistributionTask(ctx, &repositorynodepb.ExecuteDistributionTaskRequest{ + TaskId: input.TaskID, WorkerId: input.WorkerID, FencingToken: input.FencingToken, + StorageNodeId: c.nodeID, + }, client.WithTimeout(31*time.Minute)) + if err != nil { + return DistributionResult{}, err + } + return DistributionResult{ + DistributionID: response.GetDistributionId(), TMSTaskID: response.GetTmsTaskId(), + Status: response.GetStatus(), ErrorCode: response.GetErrorCode(), + ErrorMessage: response.GetErrorMessage(), + }, nil +} + +func (c *Client) PrepareTaskCommit(ctx context.Context, input PreparedRevision) ( + *repositorynodepb.PrepareTaskCommitResponse, error, +) { + return c.proxy.PrepareTaskCommit(ctx, &repositorynodepb.PrepareTaskCommitRequest{ + TaskId: input.TaskID, WorkerId: input.WorkerID, FencingToken: input.FencingToken, + RevisionId: input.RevisionID, ManifestSha256: input.ManifestSHA256, + ResultObjectId: input.ResultObjectID, ResultObjectSha256: input.ResultObjectSHA256, + PackageCount: input.PackageCount, TotalBytes: input.TotalBytes, StorageNodeId: c.nodeID, + CompletionStatus: input.CompletionStatus, ErrorCode: input.ErrorCode, ErrorMessage: input.ErrorMessage, + }) +} + +func (c *Client) ConfirmTaskCommit(ctx context.Context, input ConfirmedRevision) error { + _, err := c.proxy.ConfirmTaskCommit(ctx, &repositorynodepb.ConfirmTaskCommitRequest{ + TaskId: input.TaskID, WorkerId: input.WorkerID, FencingToken: input.FencingToken, + CommitToken: input.CommitToken, StorageNodeId: c.nodeID, + }) + return err +} + +var _ Plane = (*Client)(nil) diff --git a/manager/backend/services/footstone/repositoryworker/controlplane/client_test.go b/manager/backend/services/footstone/repositoryworker/controlplane/client_test.go new file mode 100644 index 0000000000000000000000000000000000000000..9accf42e613c7a181c564c116a9a6436ddb51c9d --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/controlplane/client_test.go @@ -0,0 +1,179 @@ +package controlplane + +import ( + "context" + "testing" + + "google.golang.org/protobuf/types/known/emptypb" + "trpc.group/trpc-go/trpc-go/client" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + repositorynodepb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repositorynode" +) + +const testNodeID = "11111111-1111-4111-8111-111111111111" + +func TestClientBindsStorageNodeAndFencingIdentity(t *testing.T) { + proxy := &recordingProxy{} + value := &Client{nodeID: testNodeID, proxy: proxy} + ctx := context.Background() + if _, err := value.ClaimTask(ctx, "worker-a", 60); err != nil { + t.Fatal(err) + } + if _, err := value.HeartbeatTask(ctx, TaskHeartbeat{TaskID: "task-a", WorkerID: "worker-a", + FencingToken: 7, LeaseSeconds: 60, CurrentStep: "download"}); err != nil { + t.Fatal(err) + } + if err := value.ReportTaskFailure(ctx, TaskFailure{TaskID: "task-a", WorkerID: "worker-a", + FencingToken: 7, Status: repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_FAILED, + ErrorCode: "SYNC_DOWNLOAD_FAILED", ResultObjectID: "task-logs/task-a/result.jsonl.zst", + ResultObjectSHA256: string(make([]byte, 64)), QuarantineCount: 2}); err != nil { + t.Fatal(err) + } + if _, err := value.ExecuteDistributionTask(ctx, DistributionExecution{TaskID: "task-d", + WorkerID: "worker-a", FencingToken: 9}); err != nil { + t.Fatal(err) + } + if _, err := value.PrepareTaskCommit(ctx, PreparedRevision{TaskID: "task-b", WorkerID: "worker-a", + FencingToken: 8, RevisionID: "revision-a", ManifestSHA256: string(make([]byte, 64)), + ResultObjectID: "result.jsonl.zst", ResultObjectSHA256: string(make([]byte, 64)), + CompletionStatus: repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_PARTIAL_SUCCESS, + ErrorCode: "SOME_PACKAGES_REJECTED", ErrorMessage: "accepted=2 rejected=1"}); err != nil { + t.Fatal(err) + } + if err := value.ConfirmTaskCommit(ctx, ConfirmedRevision{TaskID: "task-b", WorkerID: "worker-a", + FencingToken: 8, CommitToken: string(make([]byte, 64))}); err != nil { + t.Fatal(err) + } + for name, nodeID := range proxy.nodeIDs { + if nodeID != testNodeID { + t.Fatalf("%s storage_node_id = %q, want %q", name, nodeID, testNodeID) + } + } + if proxy.heartbeat.GetFencingToken() != 7 || proxy.failure.GetFencingToken() != 7 || + proxy.prepare.GetFencingToken() != 8 || proxy.confirm.GetFencingToken() != 8 || + proxy.distribution.GetFencingToken() != 9 { + t.Fatal("fencing token was not preserved across repository node RPCs") + } + if proxy.prepare.GetCompletionStatus() != + repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_PARTIAL_SUCCESS || + proxy.prepare.GetErrorCode() != "SOME_PACKAGES_REJECTED" || + proxy.prepare.GetErrorMessage() != "accepted=2 rejected=1" { + t.Fatalf("partial completion fields were not preserved: %#v", proxy.prepare) + } + if proxy.failure.GetResultObjectId() != "task-logs/task-a/result.jsonl.zst" || + proxy.failure.GetQuarantineCount() != 2 { + t.Fatalf("failure result fields were not preserved: %#v", proxy.failure) + } +} + +func TestReportTaskFailureRejectsSuccess(t *testing.T) { + value := &Client{nodeID: testNodeID, proxy: &recordingProxy{}} + err := value.ReportTaskFailure(context.Background(), TaskFailure{ + Status: repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_SUCCESS, + }) + if err == nil { + t.Fatal("ReportTaskFailure() accepted SUCCESS") + } +} + +func TestValidateControlTarget(t *testing.T) { + for _, value := range []string{"https://127.0.0.1:19443", "ip://user@127.0.0.1:19443", "ip://127.0.0.1"} { + if err := validateControlTarget(value); err == nil { + t.Fatalf("validateControlTarget(%q) accepted unsafe value", value) + } + } + if err := validateControlTarget("ip://127.0.0.1:19443"); err != nil { + t.Fatal(err) + } +} + +func TestCredentialsMustStayOutsideRepositoryData(t *testing.T) { + root := t.TempDir() + if !withinRoot(root, root+"/keys/client.key") { + t.Fatal("credential below repository-data was not detected") + } + if withinRoot(root, t.TempDir()+"/client.key") { + t.Fatal("credential outside repository-data was rejected") + } +} + +type recordingProxy struct { + nodeIDs map[string]string + heartbeat *repositorynodepb.HeartbeatTaskRequest + failure *repositorynodepb.ReportTaskFailureRequest + distribution *repositorynodepb.ExecuteDistributionTaskRequest + prepare *repositorynodepb.PrepareTaskCommitRequest + confirm *repositorynodepb.ConfirmTaskCommitRequest +} + +func (p *recordingProxy) record(name, nodeID string) { + if p.nodeIDs == nil { + p.nodeIDs = make(map[string]string) + } + p.nodeIDs[name] = nodeID +} + +func (p *recordingProxy) HeartbeatNode(context.Context, *repositorynodepb.HeartbeatNodeRequest, + ...client.Option, +) (*repositorynodepb.HeartbeatNodeResponse, error) { + return &repositorynodepb.HeartbeatNodeResponse{}, nil +} + +func (p *recordingProxy) ClaimTask(_ context.Context, req *repositorynodepb.ClaimTaskRequest, + _ ...client.Option, +) (*repositorynodepb.ClaimTaskResponse, error) { + p.record("claim", req.GetStorageNodeId()) + return &repositorynodepb.ClaimTaskResponse{}, nil +} + +func (p *recordingProxy) HeartbeatTask(_ context.Context, req *repositorynodepb.HeartbeatTaskRequest, + _ ...client.Option, +) (*repositorynodepb.HeartbeatTaskResponse, error) { + p.record("heartbeat", req.GetStorageNodeId()) + p.heartbeat = req + return &repositorynodepb.HeartbeatTaskResponse{}, nil +} + +func (p *recordingProxy) ReportTaskFailure(_ context.Context, req *repositorynodepb.ReportTaskFailureRequest, + _ ...client.Option, +) (*emptypb.Empty, error) { + p.record("failure", req.GetStorageNodeId()) + p.failure = req + return &emptypb.Empty{}, nil +} + +func (p *recordingProxy) ExecuteDistributionTask(_ context.Context, + req *repositorynodepb.ExecuteDistributionTaskRequest, _ ...client.Option, +) (*repositorynodepb.ExecuteDistributionTaskResponse, error) { + p.record("distribution", req.GetStorageNodeId()) + p.distribution = req + return &repositorynodepb.ExecuteDistributionTaskResponse{ + Status: repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_SUCCESS, + }, nil +} + +func (p *recordingProxy) PrepareTaskCommit(_ context.Context, req *repositorynodepb.PrepareTaskCommitRequest, + _ ...client.Option, +) (*repositorynodepb.PrepareTaskCommitResponse, error) { + p.record("prepare", req.GetStorageNodeId()) + p.prepare = req + return &repositorynodepb.PrepareTaskCommitResponse{}, nil +} + +func (p *recordingProxy) ConfirmTaskCommit(_ context.Context, req *repositorynodepb.ConfirmTaskCommitRequest, + _ ...client.Option, +) (*emptypb.Empty, error) { + p.record("confirm", req.GetStorageNodeId()) + p.confirm = req + return &emptypb.Empty{}, nil +} + +func (p *recordingProxy) RenewNodeCertificate(_ context.Context, + req *repositorynodepb.RenewNodeCertificateRequest, _ ...client.Option, +) (*repositorynodepb.NodeCertificateResponse, error) { + p.record("renew", req.GetStorageNodeId()) + return &repositorynodepb.NodeCertificateResponse{StorageNodeId: req.GetStorageNodeId()}, nil +} + +var _ repositorynodepb.RepositoryNodeInternalServiceClientProxy = (*recordingProxy)(nil) diff --git a/manager/backend/services/footstone/repositoryworker/distribution.go b/manager/backend/services/footstone/repositoryworker/distribution.go new file mode 100644 index 0000000000000000000000000000000000000000..964c327bb3d7f61e87bb83e7df047965906849a9 --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/distribution.go @@ -0,0 +1,325 @@ +package repositoryworker + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/google/uuid" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/tmsclient" +) + +const distributionResultMarker = "OCM_DISTRIBUTION_OK" + +func (h *Handler) handleDistribution(ctx context.Context, task *po.RepositoryTask) Outcome { + var payload request.RepositoryDistributionPayload + if err := json.Unmarshal([]byte(task.PayloadJSON), &payload); err != nil || + uuid.Validate(payload.DistributionID) != nil || len(payload.TargetIPs) == 0 { + return failed("INVALID_DISTRIBUTION_PAYLOAD", "distribution task payload is invalid") + } + distribution, err := h.distributions.Get(payload.DistributionID, nil) + if err != nil { + return failed("DISTRIBUTION_NOT_FOUND", err.Error()) + } + repository, err := h.repositories.GetByID(task.RepositoryID, nil) + if err != nil { + return failed("REPOSITORY_NOT_FOUND", err.Error()) + } + revision, err := h.content.GetRevision(repository.ID, task.RevisionID, nil) + if err != nil { + return failed("REVISION_NOT_FOUND", err.Error()) + } + if distribution.RepositoryID != repository.ID || distribution.RevisionID != revision.ID || + payload.ManifestSHA256 != distribution.ManifestSHA256 || + payload.ManifestSHA256 != revision.SnapshotManifestSHA256 || + payload.RepomdSHA256 != distribution.RepomdSHA256 || + payload.RepomdSHA256 != revision.RepomdSHA256 { + return failed("IMMUTABLE_REVISION_MISMATCH", "distribution does not match the immutable revision digest") + } + if err = h.distributions.Start(distribution.ID, task.ID, nil); err != nil { + return failed("DISTRIBUTION_START_FAILED", err.Error()) + } + artifact, err := buildDistributionArtifact(distribution, payload) + if err != nil { + return h.failDistribution(distribution, "DISTRIBUTION_SCRIPT_FAILED", err.Error()) + } + artifactMD5, err := h.tms.UploadArtifact(ctx, "repository-distribution-"+distribution.ID+".tar.gz", artifact) + if err != nil { + return h.failDistribution(distribution, "TMS_ARTIFACT_UPLOAD_FAILED", err.Error()) + } + tmsTaskID, err := h.tms.Publish(ctx, artifactMD5, payload.TargetIPs, payload.TimeoutSeconds) + if err != nil { + return h.failDistribution(distribution, "TMS_TASK_PUBLISH_FAILED", err.Error()) + } + if err = h.distributions.SetTMS(distribution.ID, tmsTaskID, artifactMD5, nil); err != nil { + cancelCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + _ = h.tms.Cancel(cancelCtx, tmsTaskID, "failed to persist repository distribution linkage") + return h.failDistribution(distribution, "DISTRIBUTION_PERSIST_FAILED", err.Error()) + } + + deadline := time.NewTimer(time.Duration(payload.TimeoutSeconds+30) * time.Second) + defer deadline.Stop() + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + cancelCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + _ = h.tms.Cancel(cancelCtx, tmsTaskID, "repository distribution canceled") + cancel() + return h.failDistribution(distribution, "DISTRIBUTION_CANCELED", "distribution was canceled") + case <-deadline.C: + cancelCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + _ = h.tms.Cancel(cancelCtx, tmsTaskID, "repository distribution timed out") + cancel() + return h.failDistribution(distribution, "TMS_TASK_TIMEOUT", "distribution did not reach a terminal state") + case <-ticker.C: + detail, detailErr := h.tms.Detail(ctx, tmsTaskID) + if detailErr != nil { + continue + } + if !isDistributionTerminal(detail.Status) { + continue + } + return h.completeDistribution(distribution, tmsTaskID, artifactMD5, detail) + } + } +} + +func (h *Handler) failDistribution(value *po.RepositoryDistribution, code, message string) Outcome { + now := h.now() + value.Status = request.RepositoryDistributionStatusFailed + targetStatus := request.RepositoryDistributionTargetFailed + if code == "DISTRIBUTION_CANCELED" { + value.Status = request.RepositoryDistributionStatusCanceled + targetStatus = request.RepositoryDistributionTargetCanceled + } else if code == "TMS_TASK_TIMEOUT" { + targetStatus = request.RepositoryDistributionTargetTimeout + } + value.ErrorCode, value.ErrorMessage, value.FinishedAt = code, message, &now + for index := range value.Targets { + if value.Targets[index].Status == request.RepositoryDistributionTargetSuccess { + continue + } + value.Targets[index].Status = targetStatus + value.Targets[index].ErrorCode = code + value.Targets[index].ErrorMessage = message + value.Targets[index].FinishedAt = &now + } + if err := h.distributions.Complete(value, value.Targets, nil); err != nil { + return failed("DISTRIBUTION_PERSIST_FAILED", err.Error()) + } + status := request.RepositoryTaskStatusFailed + if value.Status == request.RepositoryDistributionStatusCanceled { + status = request.RepositoryTaskStatusCanceled + } + return Outcome{Status: status, ErrorCode: code, ErrorMessage: message, RevisionID: value.RevisionID} +} + +func (h *Handler) completeDistribution(value *po.RepositoryDistribution, tmsTaskID, artifactMD5 string, + detail tmsclient.TaskDetail, +) Outcome { + now := h.now() + value.TMSTaskID, value.ArtifactMD5, value.FinishedAt = tmsTaskID, artifactMD5, &now + byAgent := make(map[uint64]tmsclient.TargetResult, len(detail.Targets)) + for _, item := range detail.Targets { + byAgent[item.AgentID] = item + } + succeeded := 0 + for index := range value.Targets { + target := &value.Targets[index] + item, ok := byAgent[target.AgentID] + if !ok { + target.Status, target.ErrorCode, target.ErrorMessage = request.RepositoryDistributionTargetFailed, + "TMS_TARGET_RESULT_MISSING", "TMS did not return a result for the selected target" + target.FinishedAt = &now + continue + } + target.Status, target.AttemptCount, target.ExitCode = item.Status, item.AttemptCount, item.ExitCode + target.Output, target.OutputRef, target.OutputTruncated = item.Output, item.OutputRef, item.OutputTruncated + target.ErrorCode, target.ErrorMessage = item.ErrorCode, item.ErrorMessage + if item.StartedAt > 0 { + started := time.Unix(item.StartedAt, 0).UTC() + target.StartedAt = &started + } + if item.EndedAt > 0 { + ended := time.Unix(item.EndedAt, 0).UTC() + target.FinishedAt = &ended + } + if target.Status == request.RepositoryDistributionTargetSuccess && + strings.Contains(target.Output, distributionResultMarker+" revision="+value.RevisionID) && + strings.Contains(target.Output, "manifest_sha256="+value.ManifestSHA256) && + strings.Contains(target.Output, "repomd_sha256="+value.RepomdSHA256) { + succeeded++ + } else if target.Status == request.RepositoryDistributionTargetSuccess { + target.Status, target.ErrorCode, target.ErrorMessage = request.RepositoryDistributionTargetFailed, + "DISTRIBUTION_EVIDENCE_MISSING", "agent success output did not prove the requested immutable revision" + } + } + status, distributionStatus := distributionAggregateStatus(succeeded, len(value.Targets), detail.Status) + value.Status = distributionStatus + if value.Status != request.RepositoryDistributionStatusSuccess { + if value.Status == request.RepositoryDistributionStatusCanceled { + value.ErrorCode = "DISTRIBUTION_CANCELED" + value.ErrorMessage = "TMS canceled the repository distribution" + } else { + value.ErrorCode = "DISTRIBUTION_TARGET_FAILURE" + value.ErrorMessage = fmt.Sprintf("%d of %d targets published the immutable revision", succeeded, len(value.Targets)) + } + } + if err := h.distributions.Complete(value, value.Targets, nil); err != nil { + return failed("DISTRIBUTION_PERSIST_FAILED", err.Error()) + } + return Outcome{Status: status, ErrorCode: value.ErrorCode, ErrorMessage: value.ErrorMessage, + RevisionID: value.RevisionID} +} + +func distributionAggregateStatus(succeeded, total int, tmsStatus string) (string, string) { + if total > 0 && succeeded == total { + return request.RepositoryTaskStatusSuccess, request.RepositoryDistributionStatusSuccess + } + if succeeded > 0 { + return request.RepositoryTaskStatusPartialSuccess, request.RepositoryDistributionStatusPartialSuccess + } + if tmsStatus == request.RepositoryDistributionStatusCanceled { + return request.RepositoryTaskStatusCanceled, request.RepositoryDistributionStatusCanceled + } + return request.RepositoryTaskStatusFailed, request.RepositoryDistributionStatusFailed +} + +func isDistributionTerminal(status string) bool { + switch status { + case "SUCCESS", "PARTIAL_SUCCESS", "FAILED", "CANCELED", "TIMEOUT": + return true + default: + return false + } +} + +func buildDistributionArtifact(value *po.RepositoryDistribution, + payload request.RepositoryDistributionPayload, +) ([]byte, error) { + script := fmt.Sprintf(`#!/bin/sh +set -eu +repository_id=%s +revision_id=%s +revision_url=%s +manifest_sha=%s +repomd_sha=%s +runtime_home="${HOME:-}" +if [ -z "$runtime_home" ] && command -v getent >/dev/null 2>&1; then + runtime_home=$(getent passwd "$(id -u)" | cut -d: -f6) +fi +mirror_root="${OCM_REPOSITORY_MIRROR_ROOT:-}" +if [ -z "$mirror_root" ]; then + test -n "$runtime_home" || { + echo "HOME is unavailable; configure OCM_REPOSITORY_MIRROR_ROOT for the Agent service" >&2 + exit 1 + } + mirror_root="$runtime_home/.local/share/ocmanager/repository-mirrors" +fi +case "$mirror_root" in + /*) ;; + *) echo "OCM_REPOSITORY_MIRROR_ROOT must be an absolute path" >&2; exit 1 ;; +esac +publish_root="$mirror_root/$repository_id" +staging="$publish_root/staging/$revision_id.%s" +revision="$publish_root/revisions/$revision_id" +manifest="$staging/manifest.json" +mkdir -p "$staging/content" "$publish_root/revisions" +trap 'rm -rf "$staging"' EXIT HUP INT TERM +curl -fsSL "$revision_url/manifest.json" -o "$manifest" +printf '%%s %%s\n' "$manifest_sha" "$manifest" | sha256sum -c - +python3 - "$manifest" >"$staging/files.tsv" <<'PY' +import json, pathlib, sys, urllib.parse +d = json.load(open(sys.argv[1], encoding="utf-8")) +if d.get("schema_version", 0) < 2: + raise SystemExit("manifest schema v2 required") +seen = set() +for item in d.get("packages", []) + d.get("metadata_files", []): + path = item.get("path", "") + p = pathlib.PurePosixPath(path) + if not path or p.is_absolute() or ".." in p.parts or path in seen: + raise SystemExit("unsafe or duplicate manifest path") + digest = item.get("sha256", "") + if len(digest) != 64: + raise SystemExit("invalid manifest digest") + seen.add(path) + print(path + "\t" + digest + "\t" + urllib.parse.quote(path, safe="/")) +PY +tab=$(printf '\t') +while IFS="$tab" read -r path digest url_path; do + destination="$staging/content/$path" + mkdir -p "$(dirname "$destination")" + curl -fsSL "$revision_url/$url_path" -o "$destination" + printf '%%s %%s\n' "$digest" "$destination" | sha256sum -c - +done <"$staging/files.tsv" +test -f "$staging/content/repodata/repomd.xml" +printf '%%s %%s\n' "$repomd_sha" "$staging/content/repodata/repomd.xml" | sha256sum -c - +cp "$manifest" "$staging/content/manifest.json" +if [ ! -d "$revision" ]; then + mv "$staging/content" "$revision" +else + while IFS="$tab" read -r path digest url_path; do + printf '%%s %%s\n' "$digest" "$revision/$path" | sha256sum -c - + done <"$staging/files.tsv" + printf '%%s %%s\n' "$manifest_sha" "$revision/manifest.json" | sha256sum -c - +fi +python3 - "$revision" "$staging/files.tsv" <<'PY' +import pathlib, sys +root = pathlib.Path(sys.argv[1]) +expected = {"manifest.json"} +with open(sys.argv[2], encoding="utf-8") as lines: + expected.update(line.split("\t", 1)[0] for line in lines if line.strip()) +actual = set() +for item in root.rglob("*"): + if item.is_symlink(): + raise SystemExit("revision contains a symlink") + if item.is_file(): + actual.add(item.relative_to(root).as_posix()) +if actual != expected: + raise SystemExit("revision file set does not match manifest") +PY +cache="$staging/dnf-cache" +mkdir -p "$cache/cache" "$cache/persist" +dnf -q --disablerepo='*' --repofrompath=ocm-distributed,"file://$revision" \ + --enablerepo=ocm-distributed --setopt=cachedir="$cache/cache" \ + --setopt=persistdir="$cache/persist" makecache +rm -f "$publish_root/current.next" +ln -s "revisions/$revision_id" "$publish_root/current.next" +mv -Tf "$publish_root/current.next" "$publish_root/current" +test "$(readlink "$publish_root/current")" = "revisions/$revision_id" +printf '%s revision=%%s manifest_sha256=%%s repomd_sha256=%%s current=%%s\n' \ + "$revision_id" "$manifest_sha" "$repomd_sha" "$(readlink "$publish_root/current")" +`, shellQuote(value.RepositoryID), shellQuote(value.RevisionID), shellQuote(strings.TrimRight(payload.RevisionURL, "/")), + shellQuote(value.ManifestSHA256), shellQuote(value.RepomdSHA256), shellQuote(value.ID), distributionResultMarker) + var buffer bytes.Buffer + gzipWriter, err := gzip.NewWriterLevel(&buffer, gzip.BestCompression) + if err != nil { + return nil, err + } + gzipWriter.Header.ModTime = time.Unix(0, 0) + tarWriter := tar.NewWriter(gzipWriter) + header := &tar.Header{Name: "run.sh", Mode: 0700, Size: int64(len(script)), ModTime: time.Unix(0, 0)} + if err = tarWriter.WriteHeader(header); err == nil { + _, err = tarWriter.Write([]byte(script)) + } + if closeErr := tarWriter.Close(); err == nil { + err = closeErr + } + if closeErr := gzipWriter.Close(); err == nil { + err = closeErr + } + return buffer.Bytes(), err +} + +func shellQuote(value string) string { return "'" + strings.ReplaceAll(value, "'", "'\"'\"'") + "'" } diff --git a/manager/backend/services/footstone/repositoryworker/distribution_integration_test.go b/manager/backend/services/footstone/repositoryworker/distribution_integration_test.go new file mode 100644 index 0000000000000000000000000000000000000000..a846c6881a9be26ce55545bce0fadadf8a747463 --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/distribution_integration_test.go @@ -0,0 +1,202 @@ +package repositoryworker + +import ( + "context" + "encoding/json" + "os" + "strconv" + "strings" + "testing" + "time" + + "github.com/google/uuid" + + baseconfig "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorycontentrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorydistributionrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositoryrepo" + appconfig "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" +) + +func TestDistributionThroughTMS(t *testing.T) { + dsn := os.Getenv("OCM_REPOSITORY_INTEGRATION_DSN") + tmsURL := os.Getenv("OCM_REPOSITORY_TMS_URL") + revisionURL := os.Getenv("OCM_REPOSITORY_DISTRIBUTION_REVISION_URL") + manifestSHA := os.Getenv("OCM_REPOSITORY_DISTRIBUTION_MANIFEST_SHA256") + repomdSHA := os.Getenv("OCM_REPOSITORY_DISTRIBUTION_REPOMD_SHA256") + targetIPs := strings.Fields(os.Getenv("OCM_REPOSITORY_DISTRIBUTION_TARGET_IPS")) + agentIDs := strings.Fields(os.Getenv("OCM_REPOSITORY_DISTRIBUTION_AGENT_IDS")) + if dsn == "" || tmsURL == "" || revisionURL == "" || len(targetIPs) < 1 || len(agentIDs) != len(targetIPs) { + t.Skip("repository distribution environment with one or more Agents is not configured") + } + db, err := database.NewDBClient(baseconfig.AccessPoint{Source: dsn}) + if err != nil { + t.Fatal(err) + } + defer db.Close() + repositories := repositoryrepo.New(db) + content := repositorycontentrepo.New(db) + distributions := repositorydistributionrepo.New(db) + for _, syncer := range []interface{ SyncTables() error }{repositories, content, distributions} { + if err = syncer.SyncTables(); err != nil { + t.Fatal(err) + } + } + now := time.Now().UTC() + repositoryID := envUUID("OCM_REPOSITORY_DISTRIBUTION_REPOSITORY_ID") + revisionID := envUUID("OCM_REPOSITORY_DISTRIBUTION_REVISION_ID") + distributionID := envUUID("OCM_REPOSITORY_DISTRIBUTION_ID") + repository := &po.SoftwareRepository{ + ID: repositoryID, ProjectID: 900009, SystemProduct: "OpenCloudOS", ProductVersion: "9.6", + Name: "phase9-distribution", Architecture: request.RepositoryArchitectureX8664, + Type: request.RepositoryTypePrivate, StorageLocation: "public/phase9", PublishKey: uuid.NewString()[:32], + GPGPolicy: request.RepositoryGPGDisabled, Enabled: true, Status: request.RepositoryStatusReady, + ActiveRevisionID: revisionID, Creator: "repository-phase9", + } + if err = repository.SetPackageFilters(nil, nil); err != nil { + t.Fatal(err) + } + if err = repositories.Create(repository, nil); err != nil { + t.Fatal(err) + } + if os.Getenv("OCM_REPOSITORY_KEEP_INTEGRATION_DATA") != "1" { + t.Cleanup(func() { + db.DB().Where("distribution_id = ?", distributionID).Delete(&po.RepositoryDistributionTarget{}) + db.DB().Where("id = ?", distributionID).Delete(&po.RepositoryDistribution{}) + db.DB().Where("id = ?", revisionID).Delete(&po.RepositoryRevision{}) + db.DB().Where("id = ?", repositoryID).Delete(&po.SoftwareRepository{}) + }) + } + revision := &po.RepositoryRevision{ + ID: revisionID, RepositoryID: repositoryID, TaskID: uuid.NewString(), Kind: request.RepositoryRevisionKindUpload, + Status: request.RepositoryRevisionStatusPublished, ManifestSHA256: strings.Repeat("c", 64), + RepomdSHA256: repomdSHA, SnapshotManifestSHA256: manifestSHA, + RelativePath: "public/phase9/revisions/" + revisionID, CreatedBy: "repository-phase9", PublishedAt: &now, + } + if err = content.CreateRevision(revision, nil, nil); err != nil { + t.Fatal(err) + } + distribution := &po.RepositoryDistribution{ + ID: distributionID, RepositoryID: repositoryID, ProjectID: repository.ProjectID, + RevisionID: revisionID, RepositoryTaskID: uuid.NewString(), ManifestSHA256: manifestSHA, + RepomdSHA256: repomdSHA, Status: request.RepositoryDistributionStatusPending, + RequestedBy: "repository-phase9", TimeoutSeconds: 600, + } + targets := make([]po.RepositoryDistributionTarget, 0, len(targetIPs)) + for index, ip := range targetIPs { + agentID, parseErr := strconv.ParseUint(agentIDs[index], 10, 64) + if parseErr != nil { + t.Fatal(parseErr) + } + targets = append(targets, po.RepositoryDistributionTarget{ + ID: uuid.NewString(), DistributionID: distributionID, TargetIP: ip, + AgentID: agentID, Status: request.RepositoryDistributionTargetPending, + }) + } + if err = distributions.Create(distribution, targets, nil); err != nil { + t.Fatal(err) + } + payload, _ := json.Marshal(request.RepositoryDistributionPayload{ + DistributionID: distributionID, TargetIPs: targetIPs, RevisionURL: revisionURL, + ManifestSHA256: manifestSHA, RepomdSHA256: repomdSHA, TimeoutSeconds: 600, + }) + store, err := storage.New(appconfig.RepositoryConfig{ + Root: t.TempDir(), TMSBaseURL: tmsURL, TMSUserName: "repository-phase9-acceptance", + }) + if err != nil { + t.Fatal(err) + } + handler := NewHandler(db, store) + ctx, cancel := context.WithTimeout(context.Background(), 11*time.Minute) + defer cancel() + outcome := handler.Handle(ctx, &po.RepositoryTask{ + ID: distribution.RepositoryTaskID, RepositoryID: repositoryID, RevisionID: revisionID, + Type: request.RepositoryTaskTypeDistribute, PayloadJSON: string(payload), + }) + if outcome.Status != request.RepositoryTaskStatusSuccess { + t.Fatalf("distribution outcome = %+v", outcome) + } + stored, err := distributions.Get(distributionID, nil) + if err != nil { + t.Fatal(err) + } + if stored.Status != request.RepositoryDistributionStatusSuccess || len(stored.Targets) != len(targetIPs) { + t.Fatalf("stored distribution = %+v", stored) + } + t.Logf("distribution_id=%s repository_id=%s revision_id=%s tms_task_id=%s manifest_sha256=%s repomd_sha256=%s", + stored.ID, stored.RepositoryID, stored.RevisionID, stored.TMSTaskID, + stored.ManifestSHA256, stored.RepomdSHA256) + for _, target := range stored.Targets { + if target.Status != request.RepositoryDistributionTargetSuccess || + !strings.Contains(target.Output, distributionResultMarker+" revision="+revisionID) { + t.Fatalf("target result = %+v", target) + } + t.Logf("target_ip=%s agent_id=%d status=%s exit_code=%d attempts=%d marker=%t", + target.TargetIP, target.AgentID, target.Status, target.ExitCode, target.AttemptCount, + strings.Contains(target.Output, distributionResultMarker)) + } + + badRevisionID, badDistributionID := uuid.NewString(), uuid.NewString() + badManifestSHA := strings.Repeat("f", 64) + badRevision := &po.RepositoryRevision{ + ID: badRevisionID, RepositoryID: repositoryID, TaskID: uuid.NewString(), Kind: request.RepositoryRevisionKindUpload, + Status: request.RepositoryRevisionStatusPublished, ManifestSHA256: strings.Repeat("d", 64), + RepomdSHA256: repomdSHA, SnapshotManifestSHA256: badManifestSHA, + RelativePath: "public/phase9/revisions/" + badRevisionID, CreatedBy: "repository-phase9", PublishedAt: &now, + } + if err = content.CreateRevision(badRevision, nil, nil); err != nil { + t.Fatal(err) + } + badDistribution := &po.RepositoryDistribution{ + ID: badDistributionID, RepositoryID: repositoryID, ProjectID: repository.ProjectID, + RevisionID: badRevisionID, RepositoryTaskID: uuid.NewString(), ManifestSHA256: badManifestSHA, + RepomdSHA256: repomdSHA, Status: request.RepositoryDistributionStatusPending, + RequestedBy: "repository-phase9", TimeoutSeconds: 600, + } + badTargets := make([]po.RepositoryDistributionTarget, 0, len(targets)) + for _, target := range targets { + badTargets = append(badTargets, po.RepositoryDistributionTarget{ + ID: uuid.NewString(), DistributionID: badDistributionID, TargetIP: target.TargetIP, + AgentID: target.AgentID, Status: request.RepositoryDistributionTargetPending, + }) + } + if err = distributions.Create(badDistribution, badTargets, nil); err != nil { + t.Fatal(err) + } + badPayload, _ := json.Marshal(request.RepositoryDistributionPayload{ + DistributionID: badDistributionID, TargetIPs: targetIPs, RevisionURL: revisionURL, + ManifestSHA256: badManifestSHA, RepomdSHA256: repomdSHA, TimeoutSeconds: 600, + }) + badOutcome := handler.Handle(ctx, &po.RepositoryTask{ + ID: badDistribution.RepositoryTaskID, RepositoryID: repositoryID, RevisionID: badRevisionID, + Type: request.RepositoryTaskTypeDistribute, PayloadJSON: string(badPayload), + }) + if badOutcome.Status != request.RepositoryTaskStatusFailed { + t.Fatalf("bad distribution outcome = %+v", badOutcome) + } + badStored, err := distributions.Get(badDistributionID, nil) + if err != nil { + t.Fatal(err) + } + if badStored.Status != request.RepositoryDistributionStatusFailed || len(badStored.Targets) != len(targetIPs) { + t.Fatalf("bad stored distribution = %+v", badStored) + } + for _, target := range badStored.Targets { + if target.Status != request.RepositoryDistributionTargetFailed || target.ExitCode == 0 { + t.Fatalf("bad target accepted corrupt manifest = %+v", target) + } + } + t.Logf("negative_distribution_id=%s tms_task_id=%s status=%s expected_manifest_sha256=%s", + badStored.ID, badStored.TMSTaskID, badStored.Status, badStored.ManifestSHA256) +} + +func envUUID(name string) string { + if value := strings.TrimSpace(os.Getenv(name)); uuid.Validate(value) == nil { + return value + } + return uuid.NewString() +} diff --git a/manager/backend/services/footstone/repositoryworker/distribution_test.go b/manager/backend/services/footstone/repositoryworker/distribution_test.go new file mode 100644 index 0000000000000000000000000000000000000000..09991ca2f17d7527743974e9f8aa957ac28800c9 --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/distribution_test.go @@ -0,0 +1,95 @@ +package repositoryworker + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "io" + "strings" + "testing" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +func TestBuildDistributionArtifactBindsImmutableRevision(t *testing.T) { + value := &po.RepositoryDistribution{ + ID: "11111111-1111-1111-1111-111111111111", + RepositoryID: "22222222-2222-2222-2222-222222222222", + RevisionID: "33333333-3333-3333-3333-333333333333", + ManifestSHA256: strings.Repeat("a", 64), RepomdSHA256: strings.Repeat("b", 64), + } + body, err := buildDistributionArtifact(value, request.RepositoryDistributionPayload{ + RevisionURL: "https://repo.example/repositories/key/revisions/33333333-3333-3333-3333-333333333333", + }) + if err != nil { + t.Fatalf("buildDistributionArtifact() error = %v", err) + } + gz, err := gzip.NewReader(bytes.NewReader(body)) + if err != nil { + t.Fatal(err) + } + tr := tar.NewReader(gz) + header, err := tr.Next() + if err != nil || header.Name != "run.sh" { + t.Fatalf("artifact header = %#v, %v", header, err) + } + script, err := io.ReadAll(tr) + if err != nil { + t.Fatal(err) + } + text := string(script) + for _, expected := range []string{ + "revision_id='33333333-3333-3333-3333-333333333333'", + "revision_url='https://repo.example/repositories/key/revisions/33333333-3333-3333-3333-333333333333'", + "manifest_sha='" + strings.Repeat("a", 64) + "'", + "repomd_sha='" + strings.Repeat("b", 64) + "'", + "ln -s \"revisions/$revision_id\"", + "revision file set does not match manifest", + "OCM_REPOSITORY_MIRROR_ROOT", + "getent passwd", + "OCM_DISTRIBUTION_OK", + } { + if !strings.Contains(text, expected) { + t.Fatalf("run.sh is missing %q", expected) + } + } + if strings.Contains(text, "/current/manifest.json") || strings.Contains(text, "/latest/") { + t.Fatal("run.sh must not resolve a mutable current/latest source") + } + if strings.Contains(text, "${HOME:?}") { + t.Fatal("run.sh must support Agent services that do not export HOME") + } +} + +func TestDistributionTerminalStatuses(t *testing.T) { + for _, status := range []string{"SUCCESS", "PARTIAL_SUCCESS", "FAILED", "CANCELED", "TIMEOUT"} { + if !isDistributionTerminal(status) { + t.Fatalf("%s must be terminal", status) + } + } + if isDistributionTerminal("RUNNING") { + t.Fatal("RUNNING must not be terminal") + } +} + +func TestDistributionAggregateStatus(t *testing.T) { + tests := []struct { + succeeded, total int + tmsStatus string + wantTask string + wantDistribution string + }{ + {2, 2, "SUCCESS", request.RepositoryTaskStatusSuccess, request.RepositoryDistributionStatusSuccess}, + {1, 2, "PARTIAL_SUCCESS", request.RepositoryTaskStatusPartialSuccess, request.RepositoryDistributionStatusPartialSuccess}, + {0, 2, "CANCELED", request.RepositoryTaskStatusCanceled, request.RepositoryDistributionStatusCanceled}, + {0, 2, "FAILED", request.RepositoryTaskStatusFailed, request.RepositoryDistributionStatusFailed}, + } + for _, test := range tests { + taskStatus, distributionStatus := distributionAggregateStatus(test.succeeded, test.total, test.tmsStatus) + if taskStatus != test.wantTask || distributionStatus != test.wantDistribution { + t.Fatalf("aggregate(%d/%d, %s) = %s/%s", test.succeeded, test.total, + test.tmsStatus, taskStatus, distributionStatus) + } + } +} diff --git a/manager/backend/services/footstone/repositoryworker/dnf_e2e_integration_test.go b/manager/backend/services/footstone/repositoryworker/dnf_e2e_integration_test.go new file mode 100644 index 0000000000000000000000000000000000000000..95ee84fbf42d0c678fad9403ed29b3b8efb53e14 --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/dnf_e2e_integration_test.go @@ -0,0 +1,547 @@ +package repositoryworker + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "path/filepath" + "sort" + "strings" + "testing" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + baseconfig "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + ftpb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/footstone" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorycontentrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorystoragenoderepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryContentService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryTaskService" + appconfig "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" +) + +const phase7ProjectID int64 = 900007 +const phase7StorageNodeID = "90000700-0000-4000-8000-000000000001" + +type dnfE2EReport struct { + SchemaVersion int `json:"schema_version"` + Phase string `json:"phase"` + Status string `json:"status"` + GeneratedAt string `json:"generated_at"` + Environment dnfE2EEnvironment `json:"environment"` + Repositories []dnfE2ERepository `json:"repositories"` + Cases []dnfE2ECase `json:"cases"` + Summary dnfE2ESummary `json:"summary"` +} + +type dnfE2EEnvironment struct { + HostOS string `json:"host_os"` + ContainerOS string `json:"container_os"` + ContainerImage string `json:"container_image"` + Isolation string `json:"isolation"` +} + +type dnfE2ERepository struct { + Role string `json:"role"` + RepositoryID string `json:"repository_id"` + RevisionID string `json:"revision_id"` + ManifestSHA256 string `json:"manifest_sha256"` + RepoID string `json:"repo_id"` + BaseURL string `json:"baseurl"` + ConfigFile string `json:"config_file"` +} + +type dnfE2ECase struct { + Name string `json:"case_name"` + Expected string `json:"expected"` + Actual string `json:"actual"` + Status string `json:"status"` + RepositoryIDs []string `json:"repository_ids"` + RepoIDs []string `json:"repo_ids"` + BaseURLs []string `json:"baseurls"` + PackageNEVRAs map[string]string `json:"package_nevras"` + SourceRepos map[string]string `json:"source_repos"` + DNFCommand string `json:"dnf_command"` + DNFExitCode int `json:"dnf_exit_code"` + InstallResult string `json:"install_result"` + DurationMS int64 `json:"duration_ms"` + FailedPackages []string `json:"failed_packages"` + RPMQueryResults map[string]string `json:"rpm_query_results"` + LogDigest string `json:"log_digest"` + LogSummary string `json:"log_summary"` + LogPath string `json:"log_path"` +} + +type dnfE2ESummary struct { + Passed int `json:"passed"` + Failed int `json:"failed"` + Total int `json:"total"` +} + +type dnfE2ECaseSpec struct { + Name string + Expected string + Configs []*request.GeneratedRepositoryConfig + Packages []string + ExpectedRepos map[string]string + OutputDir string +} + +func TestRepositoryIsolatedDNFInstall(t *testing.T) { + dsn := os.Getenv("OCM_REPOSITORY_INTEGRATION_DSN") + image := strings.TrimSpace(os.Getenv("OCM_REPOSITORY_E2E_IMAGE")) + fixtures := map[string]string{ + "ocm-sync-only": os.Getenv("OCM_REPOSITORY_E2E_SYNC_ONLY_RPM"), + "ocm-sync-dependency": os.Getenv("OCM_REPOSITORY_E2E_SYNC_DEPENDENCY_RPM"), + "ocm-private-only": os.Getenv("OCM_REPOSITORY_E2E_PRIVATE_ONLY_RPM"), + "ocm-private-app": os.Getenv("OCM_REPOSITORY_E2E_PRIVATE_APP_RPM"), + } + if dsn == "" || image == "" || hasEmptyFixture(fixtures) { + t.Skip("repository DNF E2E environment is not configured") + } + if _, err := exec.LookPath("docker"); err != nil { + t.Skip("docker is required for clean OpenCloudOS 9 case isolation") + } + containerOS := validateOpenCloudOS9Image(t, image) + + reportPath := strings.TrimSpace(os.Getenv("OCM_REPOSITORY_E2E_REPORT")) + report := dnfE2EReport{ + SchemaVersion: 1, + Phase: "Phase 7 Isolated DNF Validation", + Status: "FAIL", + GeneratedAt: time.Now().UTC().Format(time.RFC3339), + Environment: dnfE2EEnvironment{ + HostOS: readOSRelease(), + ContainerOS: containerOS, + ContainerImage: image, + Isolation: "one disposable OpenCloudOS 9 container per case; all default repositories disabled", + }, + } + defer func() { + if reportPath != "" { + if err := writeDNFE2EReport(reportPath, report); err != nil { + t.Errorf("write DNF E2E report: %v", err) + } + } + }() + + db, err := database.NewDBClient(baseconfig.AccessPoint{Source: dsn}) + if err != nil { + t.Fatal(err) + } + defer db.Close() + if err = db.DB().AutoMigrate(&po.Project{}); err != nil { + t.Fatal(err) + } + if err = db.DB().Exec(`INSERT IGNORE INTO projects + (id, created_at, updated_at, name, en_name, creator, organization) + VALUES (?, NOW(), NOW(), 'repository-phase7', 'repository-phase7', 'admin', 'test')`, + phase7ProjectID).Error; err != nil { + t.Fatal(err) + } + + repositories := repositoryService.New(db) + tasks := repositoryTaskService.New(db) + contentRepo := repositorycontentrepo.New(db) + nodes := repositorystoragenoderepo.New(db) + if err = repositories.SyncTables(); err != nil { + t.Fatal(err) + } + if err = nodes.SyncTables(); err != nil { + t.Fatal(err) + } + if err = tasks.SyncTables(); err != nil { + t.Fatal(err) + } + if err = contentRepo.SyncTables(); err != nil { + t.Fatal(err) + } + if err = db.DB().Create(&po.RepositoryStorageNode{ + ID: phase7StorageNodeID, ProjectID: phase7ProjectID, Name: "phase7-local-node", + ControlBaseURL: "https://127.0.0.1:19443", PublishBaseURL: "http://127.0.0.1:18090", + Status: request.RepositoryStorageNodeStatusOnline, Enabled: true, CreatedBy: "admin", + }).Error; err != nil { + t.Fatal(err) + } + + upstreamRoot := t.TempDir() + copyFixture(t, fixtures["ocm-sync-only"], upstreamRoot) + copyFixture(t, fixtures["ocm-sync-dependency"], upstreamRoot) + if output, commandErr := exec.Command("createrepo_c", "--checksum", "sha256", upstreamRoot).CombinedOutput(); commandErr != nil { + t.Fatalf("create Phase 7 upstream metadata: %v: %s", commandErr, output) + } + upstream := httptest.NewServer(http.FileServer(http.Dir(upstreamRoot))) + defer upstream.Close() + + storeRoot := t.TempDir() + t.Cleanup(func() { makeIntegrationTreeWritable(storeRoot) }) + published := httptest.NewServer(http.FileServer(http.Dir(filepath.Join(storeRoot, "public")))) + defer published.Close() + store, err := storage.New(appconfig.RepositoryConfig{ + Root: storeRoot, MaxUploadBytes: 64 << 20, MaxChunkBytes: 1 << 20, + MaxArchiveFiles: 20, MaxExpandedBytes: 128 << 20, + MaxSyncBytes: 64 << 20, MaxSyncPackages: 20, + MetadataReserve: 1 << 20, MinimumFreeBytes: 1, PublicBaseURL: published.URL, + }) + if err != nil { + t.Fatal(err) + } + + ctx := entity.NewDefaultContext() + ctx.User = &ftpb.UserInfo{LoginName: "admin", Roles: []*ftpb.ModuleRole{{RoleId: request.DefaultGlobalRoleName}}} + syncRepository, err := repositories.Create(ctx, request.SoftwareRepositorySpec{ + ProjectID: phase7ProjectID, SystemProduct: "OpenCloudOS", ProductVersion: "9.6", + Name: "phase7-sync", Description: "Phase 7 isolated DNF sync repository", + Architecture: request.RepositoryArchitectureX8664, Type: request.RepositoryTypeBaseline, + UpstreamURL: upstream.URL, IncludePackages: []string{"ocm-sync-*"}, + GPGPolicy: request.RepositoryGPGDisabled, Enabled: true, PrimaryStorageNodeID: phase7StorageNodeID, + }) + if err != nil { + t.Fatal(err) + } + privateRepository, err := repositories.Create(ctx, request.SoftwareRepositorySpec{ + ProjectID: phase7ProjectID, SystemProduct: "OpenCloudOS", ProductVersion: "9.6", + Name: "phase7-private", Description: "Phase 7 isolated DNF private repository", + Architecture: request.RepositoryArchitectureX8664, Type: request.RepositoryTypePrivate, + AllowUpload: true, GPGPolicy: request.RepositoryGPGDisabled, Enabled: true, + PrimaryStorageNodeID: phase7StorageNodeID, + }) + if err != nil { + t.Fatal(err) + } + + content := repositoryContentService.New(db, repositories, tasks, store) + handler := NewHandler(db, store) + syncTask, err := content.SyncRepository(ctx, syncRepository.ID) + if err != nil { + t.Fatal(err) + } + syncOutcome := executeIntegrationTask(t, tasks, handler, syncTask.ID) + if syncOutcome.Status != request.RepositoryTaskStatusSuccess { + t.Fatalf("Phase 7 sync outcome=%+v", syncOutcome) + } + for _, packageName := range []string{"ocm-private-only", "ocm-private-app"} { + data, readErr := os.ReadFile(fixtures[packageName]) + if readErr != nil { + t.Fatal(readErr) + } + uploadTask := uploadIntegrationData(t, content, ctx, privateRepository.ID, + filepath.Base(fixtures[packageName]), data) + outcome := executeIntegrationTask(t, tasks, handler, uploadTask.ID) + if outcome.Status != request.RepositoryTaskStatusSuccess { + t.Fatalf("Phase 7 upload %s outcome=%+v", packageName, outcome) + } + } + + syncConfig, err := content.GenerateRepositoryConfig(ctx, syncRepository.ID, true) + if err != nil { + t.Fatal(err) + } + privateConfig, err := content.GenerateRepositoryConfig(ctx, privateRepository.ID, true) + if err != nil { + t.Fatal(err) + } + outputDir := filepath.Dir(reportPath) + if reportPath == "" { + outputDir = t.TempDir() + } + writeGeneratedRepositoryConfig(t, outputDir, syncConfig) + writeGeneratedRepositoryConfig(t, outputDir, privateConfig) + report.Repositories = append(report.Repositories, + reportRepository(t, contentRepo, "sync", syncConfig), + reportRepository(t, contentRepo, "private", privateConfig), + ) + caseSpecs := []dnfE2ECaseSpec{ + { + Name: "Case A - sync repository only", Expected: "ocm-sync-only installs only from the generated sync repository", + Configs: []*request.GeneratedRepositoryConfig{syncConfig}, Packages: []string{"ocm-sync-only"}, + ExpectedRepos: map[string]string{"ocm-sync-only": syncConfig.RepoID}, OutputDir: outputDir, + }, + { + Name: "Case B - private repository only", Expected: "ocm-private-only installs only from the generated private repository", + Configs: []*request.GeneratedRepositoryConfig{privateConfig}, Packages: []string{"ocm-private-only"}, + ExpectedRepos: map[string]string{"ocm-private-only": privateConfig.RepoID}, OutputDir: outputDir, + }, + { + Name: "Case C - private package with sync dependency", + Expected: "ocm-private-app installs from private and resolves ocm-sync-dependency from sync", + Configs: []*request.GeneratedRepositoryConfig{syncConfig, privateConfig}, + Packages: []string{"ocm-private-app", "ocm-sync-dependency"}, + ExpectedRepos: map[string]string{ + "ocm-private-app": privateConfig.RepoID, "ocm-sync-dependency": syncConfig.RepoID, + }, OutputDir: outputDir, + }, + } + + for index, spec := range caseSpecs { + result := runIsolatedDNFCase(t, image, index, spec) + report.Cases = append(report.Cases, result) + if result.Status == "PASS" { + report.Summary.Passed++ + } else { + report.Summary.Failed++ + } + } + report.Summary.Total = len(report.Cases) + if report.Summary.Failed == 0 && report.Summary.Passed == len(caseSpecs) { + report.Status = "PASS" + } + if reportPath != "" { + if err = writeDNFE2EReport(reportPath, report); err != nil { + t.Fatal(err) + } + } + if report.Status != "PASS" { + t.Fatalf("isolated DNF cases failed: passed=%d failed=%d report=%s", + report.Summary.Passed, report.Summary.Failed, reportPath) + } + t.Logf("Phase 7 isolated DNF validation PASS: %d/%d report=%s", + report.Summary.Passed, report.Summary.Total, reportPath) +} + +func hasEmptyFixture(fixtures map[string]string) bool { + for _, value := range fixtures { + if strings.TrimSpace(value) == "" { + return true + } + } + return false +} + +func copyFixture(t *testing.T, source, destination string) { + t.Helper() + data, err := os.ReadFile(source) + if err != nil { + t.Fatal(err) + } + if err = os.WriteFile(filepath.Join(destination, filepath.Base(source)), data, 0644); err != nil { + t.Fatal(err) + } +} + +func validateOpenCloudOS9Image(t *testing.T, image string) string { + t.Helper() + command := exec.Command("docker", "run", "--rm", image, "bash", "-lc", + `. /etc/os-release; printf '%s|%s|%s' "$ID" "$VERSION_ID" "$PRETTY_NAME"`) + output, err := command.CombinedOutput() + if err != nil { + t.Fatalf("inspect DNF E2E container OS: %v: %s", err, output) + } + parts := strings.SplitN(strings.TrimSpace(string(output)), "|", 3) + if len(parts) != 3 || parts[0] != "opencloudos" || !strings.HasPrefix(parts[1], "9.") { + t.Fatalf("DNF E2E image must be OpenCloudOS 9, got %q", strings.TrimSpace(string(output))) + } + return parts[2] +} + +func writeGeneratedRepositoryConfig(t *testing.T, outputDir string, config *request.GeneratedRepositoryConfig) { + t.Helper() + if err := os.MkdirAll(outputDir, 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(outputDir, config.FileName), []byte(config.Content), 0644); err != nil { + t.Fatal(err) + } +} + +func reportRepository(t *testing.T, content repositorycontentrepo.Repository, role string, + config *request.GeneratedRepositoryConfig, +) dnfE2ERepository { + t.Helper() + if !config.ImmutableRevision || config.BaseURL == "" || config.BaseURL != config.RevisionURL { + t.Fatalf("DNF E2E must use an immutable generated repository config: %+v", config) + } + revision, err := content.GetRevision(config.RepositoryID, config.ActiveRevisionID, nil) + if err != nil { + t.Fatal(err) + } + return dnfE2ERepository{ + Role: role, RepositoryID: config.RepositoryID, RevisionID: revision.ID, + ManifestSHA256: revision.SnapshotManifestSHA256, RepoID: config.RepoID, + BaseURL: config.BaseURL, ConfigFile: config.FileName, + } +} + +func runIsolatedDNFCase(t *testing.T, image string, index int, spec dnfE2ECaseSpec) dnfE2ECase { + t.Helper() + result := dnfE2ECase{ + Name: spec.Name, Expected: spec.Expected, Status: "FAIL", DNFExitCode: -1, + PackageNEVRAs: map[string]string{}, SourceRepos: map[string]string{}, + RPMQueryResults: map[string]string{}, FailedPackages: []string{}, + } + configDir := t.TempDir() + for _, config := range spec.Configs { + if err := os.WriteFile(filepath.Join(configDir, config.FileName), []byte(config.Content), 0600); err != nil { + t.Fatal(err) + } + result.RepositoryIDs = append(result.RepositoryIDs, config.RepositoryID) + result.RepoIDs = append(result.RepoIDs, config.RepoID) + result.BaseURLs = append(result.BaseURLs, config.BaseURL) + } + + repoIDs := strings.Join(result.RepoIDs, ",") + packages := strings.Join(spec.Packages, " ") + result.DNFCommand = fmt.Sprintf("dnf --disablerepo='*' --enablerepo='%s' --setopt=install_weak_deps=False -y install %s", + repoIDs, spec.Packages[0]) + script := fmt.Sprintf(`set -euo pipefail +dnf -q --disablerepo='*' --enablerepo='%s' clean all +printf '%s\n' +dnf -q --disablerepo='*' --enablerepo='%s' repoquery --available --qf '%%{name}|%%{epoch}:%%{version}-%%{release}.%%{arch}|%%{repoid}' %s +printf '%s\n' +dnf -v --disablerepo='*' --enablerepo='%s' --setopt=install_weak_deps=False -y install %s +printf '%s\n' +rpm -q --qf '%%{name}|%%{epochnum}:%%{version}-%%{release}.%%{arch}\n' %s +printf '%s\n' +`, repoIDs, dnfSourceBegin, repoIDs, packages, dnfSourceEnd, repoIDs, spec.Packages[0], + dnfRPMBegin, packages, dnfRPMEnd) + started := time.Now() + command := exec.Command("docker", "run", "--rm", "--network", "host", + "-v", configDir+":/etc/yum.repos.d:ro,Z", image, "bash", "-lc", script) + output, err := command.CombinedOutput() + result.DurationMS = time.Since(started).Milliseconds() + result.DNFExitCode = commandExitCode(err) + result.InstallResult = "FAIL" + result.LogDigest = sha256Text(output) + result.LogSummary = tailText(string(output), 20, 2400) + result.LogPath = fmt.Sprintf("case-%c.log", 'a'+rune(index)) + if writeErr := os.MkdirAll(spec.OutputDir, 0755); writeErr != nil { + t.Fatal(writeErr) + } + if writeErr := os.WriteFile(filepath.Join(spec.OutputDir, result.LogPath), output, 0644); writeErr != nil { + t.Fatal(writeErr) + } + + result.SourceRepos, result.PackageNEVRAs = parseSourceSection(string(output)) + result.RPMQueryResults = parseRPMSection(string(output)) + for _, name := range spec.Packages { + if result.SourceRepos[name] != spec.ExpectedRepos[name] || result.RPMQueryResults[name] == "" || + result.PackageNEVRAs[name] == "" || strings.Contains(result.PackageNEVRAs[name], "%{") { + result.FailedPackages = append(result.FailedPackages, name) + } + } + sort.Strings(result.FailedPackages) + if err == nil && len(result.FailedPackages) == 0 { + result.Status = "PASS" + result.InstallResult = "PASS" + result.Actual = "all packages installed and provider/source repositories match the generated repository IDs" + } else { + result.Actual = fmt.Sprintf("dnf_exit_code=%d failed_packages=%s", result.DNFExitCode, + strings.Join(result.FailedPackages, ",")) + } + return result +} + +const ( + dnfSourceBegin = "__OCM_SOURCE_BEGIN__" + dnfSourceEnd = "__OCM_SOURCE_END__" + dnfRPMBegin = "__OCM_RPM_BEGIN__" + dnfRPMEnd = "__OCM_RPM_END__" +) + +func parseSourceSection(output string) (map[string]string, map[string]string) { + sources := map[string]string{} + nevras := map[string]string{} + for _, line := range markedLines(output, dnfSourceBegin, dnfSourceEnd) { + parts := strings.Split(strings.TrimSpace(line), "|") + if len(parts) != 3 { + continue + } + sources[parts[0]] = parts[2] + nevras[parts[0]] = strings.Replace(parts[1], "(none):", "0:", 1) + } + return sources, nevras +} + +func parseRPMSection(output string) map[string]string { + values := map[string]string{} + for _, line := range markedLines(output, dnfRPMBegin, dnfRPMEnd) { + parts := strings.SplitN(strings.TrimSpace(line), "|", 2) + if len(parts) == 2 { + values[parts[0]] = parts[1] + } + } + return values +} + +func markedLines(output, begin, end string) []string { + start := strings.Index(output, begin) + if start < 0 { + return nil + } + remaining := output[start+len(begin):] + finish := strings.Index(remaining, end) + if finish < 0 { + return nil + } + return strings.Split(strings.TrimSpace(remaining[:finish]), "\n") +} + +func commandExitCode(err error) int { + if err == nil { + return 0 + } + if exitError, ok := err.(*exec.ExitError); ok { + return exitError.ExitCode() + } + return -1 +} + +func sha256Text(value []byte) string { + digest := sha256.Sum256(value) + return hex.EncodeToString(digest[:]) +} + +func tailText(value string, lines, maxBytes int) string { + parts := strings.Split(strings.TrimSpace(value), "\n") + if len(parts) > lines { + parts = parts[len(parts)-lines:] + } + result := strings.Join(parts, "\n") + if len(result) > maxBytes { + result = result[len(result)-maxBytes:] + } + return result +} + +func readOSRelease() string { + data, err := os.ReadFile("/etc/os-release") + if err != nil { + return "unknown" + } + name, version := "", "" + for _, line := range strings.Split(string(data), "\n") { + key, value, ok := strings.Cut(line, "=") + if !ok { + continue + } + value = strings.Trim(value, `"`) + switch key { + case "NAME": + name = value + case "VERSION_ID": + version = value + } + } + return strings.TrimSpace(name + " " + version) +} + +func writeDNFE2EReport(path string, report dnfE2EReport) error { + data, err := json.MarshalIndent(report, "", " ") + if err != nil { + return err + } + if err = os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return err + } + return os.WriteFile(path, append(data, '\n'), 0644) +} diff --git a/manager/backend/services/footstone/repositoryworker/gpg.go b/manager/backend/services/footstone/repositoryworker/gpg.go new file mode 100644 index 0000000000000000000000000000000000000000..1604c09493ecebb537899477b32d5222794dd08f --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/gpg.go @@ -0,0 +1,232 @@ +package repositoryworker + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +const rpmSignatureQuery = "%{SIGPGP:pgpsig}|%{SIGGPG:pgpsig}|%{RSAHEADER:pgpsig}|%{DSAHEADER:pgpsig}" + +var rpmSignatureKeyIDPattern = regexp.MustCompile(`(?i)key id ([0-9a-f]{8,16})`) +var authorizedKeyIDPattern = regexp.MustCompile(`^[0-9A-F]{8,16}$`) + +type signatureKeyReference struct { + KeyID string + Fingerprint string + PublicKeyPath string +} + +type signatureSession struct { + rpmRoot string + authorizedKeyIDs []string + enforce bool +} + +type signatureEvaluation struct { + Status string + SignerKeyID string + Message string +} + +type signatureVerificationError struct { + Code string + Message string +} + +func (e *signatureVerificationError) Error() string { return e.Message } + +func (h *Handler) newSignatureSession(ctx context.Context, task *po.RepositoryTask, + repository *po.SoftwareRepository, +) (*signatureSession, error) { + if repository.GPGPolicy == request.RepositoryGPGDisabled { + return nil, nil + } + if !request.RepositoryGPGPolicyEnabled(repository.GPGPolicy) { + return nil, fmt.Errorf("unsupported repository GPG policy %q", repository.GPGPolicy) + } + keys, err := h.content.ListGPGKeys(repository.ID, nil) + if err != nil { + return nil, fmt.Errorf("list authorized repository GPG keys: %w", err) + } + if len(keys) == 0 && request.RepositoryGPGPolicyEnforced(repository.GPGPolicy) { + return nil, fmt.Errorf("repository GPG policy %s has no authorized public key", repository.GPGPolicy) + } + references := make([]signatureKeyReference, 0, len(keys)) + for _, key := range keys { + references = append(references, signatureKeyReference{KeyID: key.KeyID, + Fingerprint: key.Fingerprint, PublicKeyPath: key.PublicKeyPath}) + } + return newSignatureSessionFromReferencesWithPolicy(ctx, h.store, task.ID, repository.ID, + repository.PublishKey, repository.GPGPolicy, references) +} + +func newSignatureSessionFromReferences(ctx context.Context, store interface { + GPGKeyRelativePath(string, string) (string, error) + VerifyGPGPublicKey(string, string, string, string) error + Resolve(string) (string, error) +}, taskID, repositoryID, publishKey string, keys []signatureKeyReference, +) (*signatureSession, error) { + return newSignatureSessionFromReferencesWithPolicy(ctx, store, taskID, repositoryID, publishKey, + request.RepositoryGPGVerifyPackage, keys) +} + +func newSignatureSessionFromReferencesWithPolicy(ctx context.Context, store interface { + GPGKeyRelativePath(string, string) (string, error) + VerifyGPGPublicKey(string, string, string, string) error + Resolve(string) (string, error) +}, taskID, repositoryID, publishKey, policy string, keys []signatureKeyReference, +) (*signatureSession, error) { + if !request.RepositoryGPGPolicyEnabled(policy) { + return nil, fmt.Errorf("unsupported repository GPG policy %q", policy) + } + if len(keys) > 64 || (request.RepositoryGPGPolicyEnforced(policy) && len(keys) == 0) { + return nil, fmt.Errorf("repository GPG policy %s requires 1 to 64 authorized public keys", policy) + } + relative := filepath.ToSlash(filepath.Join("staging", "gpg", taskID, "rpm-root")) + root, err := store.Resolve(relative) + if err != nil { + return nil, err + } + if err = os.RemoveAll(root); err != nil { + return nil, err + } + if err = os.MkdirAll(filepath.Join(root, "var", "lib", "rpm"), 0700); err != nil { + return nil, err + } + session := &signatureSession{rpmRoot: root, authorizedKeyIDs: make([]string, 0, len(keys)), + enforce: request.RepositoryGPGPolicyEnforced(policy)} + seen := make(map[string]struct{}, len(keys)) + for _, key := range keys { + key.KeyID = strings.ToUpper(strings.TrimSpace(key.KeyID)) + key.Fingerprint = strings.ToUpper(strings.TrimSpace(key.Fingerprint)) + if !authorizedKeyIDPattern.MatchString(key.KeyID) { + session.Close() + return nil, fmt.Errorf("repository GPG key %s has an invalid key ID", key.Fingerprint) + } + if _, duplicate := seen[key.Fingerprint]; duplicate { + session.Close() + return nil, fmt.Errorf("repository GPG key %s is duplicated", key.Fingerprint) + } + seen[key.Fingerprint] = struct{}{} + expectedRelative, pathErr := store.GPGKeyRelativePath(repositoryID, key.Fingerprint) + if pathErr != nil || expectedRelative != key.PublicKeyPath { + session.Close() + return nil, fmt.Errorf("repository GPG key %s has invalid storage metadata", key.Fingerprint) + } + if pathErr = store.VerifyGPGPublicKey(repositoryID, publishKey, key.Fingerprint, + key.PublicKeyPath); pathErr != nil { + session.Close() + return nil, fmt.Errorf("verify repository GPG key %s: %w", key.Fingerprint, pathErr) + } + path, pathErr := store.Resolve(key.PublicKeyPath) + if pathErr != nil { + session.Close() + return nil, pathErr + } + command := exec.CommandContext(ctx, "rpmkeys", "--root", root, "--dbpath", "/var/lib/rpm", "--import", path) + command.Env = commandEnvironment() + if output, commandErr := command.CombinedOutput(); commandErr != nil { + session.Close() + return nil, fmt.Errorf("import authorized GPG key %s: %s", key.Fingerprint, truncateOutput(output)) + } + session.authorizedKeyIDs = append(session.authorizedKeyIDs, + strings.ToUpper(key.KeyID), strings.ToUpper(key.Fingerprint)) + } + return session, nil +} + +func (s *signatureSession) Close() { + if s != nil && s.rpmRoot != "" { + _ = os.RemoveAll(filepath.Dir(s.rpmRoot)) + } +} + +func (s *signatureSession) Verify(ctx context.Context, rpmPath string) error { + if s == nil { + return nil + } + _, err := s.Evaluate(ctx, rpmPath) + return err +} + +func (s *signatureSession) Evaluate(ctx context.Context, rpmPath string) (signatureEvaluation, error) { + if s == nil { + return signatureEvaluation{Status: request.RepositoryPackageSignatureUnchecked}, nil + } + query := exec.CommandContext(ctx, "rpm", "-qp", "--qf", rpmSignatureQuery, rpmPath) + query.Env = commandEnvironment() + output, err := query.CombinedOutput() + if err != nil { + return s.evaluationFailure(request.RepositoryPackageSignatureBad, "", + request.RepositoryQuarantineBadSignature, "RPM signature header query failed: "+truncateOutput(output)) + } + signature := strings.TrimSpace(string(output)) + if signature == "" || signature == "(none)|(none)|(none)|(none)" { + return s.evaluationFailure(request.RepositoryPackageSignatureUnsigned, "", + request.RepositoryQuarantineUnsignedRPM, "RPM is not signed") + } + signerKeyID := "" + if match := rpmSignatureKeyIDPattern.FindStringSubmatch(signature); len(match) == 2 { + signerKeyID = strings.ToUpper(match[1]) + if !s.authorizes(signerKeyID) { + return s.evaluationFailure(request.RepositoryPackageSignatureUnauthorized, signerKeyID, + request.RepositoryQuarantineUnauthorizedKey, + fmt.Sprintf("RPM signer key ID %s is not authorized for this repository", signerKeyID)) + } + } + verify := exec.CommandContext(ctx, "rpmkeys", "--root", s.rpmRoot, "--dbpath", "/var/lib/rpm", + "--checksig", rpmPath) + verify.Env = commandEnvironment() + verifyOutput, verifyErr := verify.CombinedOutput() + if verifyErr != nil { + message := truncateOutput(verifyOutput) + if signerKeyID == "" && strings.Contains(strings.ToUpper(message), "NOKEY") { + return s.evaluationFailure(request.RepositoryPackageSignatureUnauthorized, signerKeyID, + request.RepositoryQuarantineUnauthorizedKey, + "RPM signature was made by a key that is not authorized for this repository") + } + return s.evaluationFailure(request.RepositoryPackageSignatureBad, signerKeyID, + request.RepositoryQuarantineBadSignature, "RPM signature verification failed: "+message) + } + return signatureEvaluation{Status: request.RepositoryPackageSignatureValid, + SignerKeyID: signerKeyID, Message: "RPM signature is valid and authorized"}, nil +} + +func (s *signatureSession) evaluationFailure(status, signerKeyID, code, message string) (signatureEvaluation, error) { + result := signatureEvaluation{Status: status, SignerKeyID: signerKeyID, Message: message} + if !s.enforce { + return result, nil + } + return result, &signatureVerificationError{Code: code, Message: message} +} + +func evaluateCandidateSignature(ctx context.Context, verifier *signatureSession, rpmPath string, + item *candidate, +) error { + result, err := verifier.Evaluate(ctx, rpmPath) + item.SignatureStatus = result.Status + item.SignerKeyID = result.SignerKeyID + item.SignatureMessage = result.Message + return err +} + +func (s *signatureSession) authorizes(keyID string) bool { + for _, authorized := range s.authorizedKeyIDs { + if strings.HasSuffix(authorized, keyID) || strings.HasSuffix(keyID, authorized) { + return true + } + } + return false +} + +func commandEnvironment() []string { + return append(os.Environ(), "LC_ALL=C", "LANG=C") +} diff --git a/manager/backend/services/footstone/repositoryworker/gpg_integration_test.go b/manager/backend/services/footstone/repositoryworker/gpg_integration_test.go new file mode 100644 index 0000000000000000000000000000000000000000..56d38786891d7be21c679ea35eef4e12582c66f2 --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/gpg_integration_test.go @@ -0,0 +1,335 @@ +package repositoryworker + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/google/uuid" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + baseconfig "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + ftpb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/footstone" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorycontentrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositoryrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryContentService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryTaskService" + appconfig "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" +) + +type gpgIntegrationFixture struct { + db *database.DBClient + repositories repositoryrepo.Repository + contentRepo repositorycontentrepo.Repository + tasks repositoryTaskService.Service + content repositoryContentService.Service + handler *Handler + manager *integrationRepositoryManager + store *storage.Store + ctx entity.Context + server *httptest.Server +} + +func TestGPGProtectedUploadAndDNFInstall(t *testing.T) { + fixture, signedRPM, unsignedRPM, unauthorizedRPM := newGPGIntegrationFixture(t) + deletable := fixture.createRepository(t, request.RepositoryTypePrivate, "", true) + deletableKey := fixture.registerAuthorizedKey(t, deletable) + if err := fixture.content.DeleteGPGKey(fixture.ctx, deletable.ID, deletableKey.ID, false); !errors.Is(err, repositoryContentService.ErrGPGKeyDeleteConfirm) { + t.Fatalf("unconfirmed key deletion error=%v", err) + } + if err := fixture.content.DeleteGPGKey(fixture.ctx, deletable.ID, deletableKey.ID, true); err != nil { + t.Fatal(err) + } + if keys, err := fixture.content.ListGPGKeys(fixture.ctx, deletable.ID); err != nil || len(keys) != 0 { + t.Fatalf("keys after deletion=%+v err=%v", keys, err) + } + emptyActive := fixture.createRepository(t, request.RepositoryTypePrivate, "", true) + emptyActiveKey := fixture.registerAuthorizedKey(t, emptyActive) + emptyActive.ActiveRevisionID = uuid.NewString() + if err := fixture.content.DeleteGPGKey(fixture.ctx, emptyActive.ID, emptyActiveKey.ID, true); err != nil { + t.Fatalf("delete key from empty active revision: %v", err) + } + repository := fixture.createRepository(t, request.RepositoryTypePrivate, "", true) + registeredKey := fixture.registerAuthorizedKey(t, repository) + + signed := readFixture(t, signedRPM) + positiveTask := uploadIntegrationData(t, fixture.content, fixture.ctx, repository.ID, + filepath.Base(signedRPM), signed) + positive := executeIntegrationTask(t, fixture.tasks, fixture.handler, positiveTask.ID) + if positive.Status != request.RepositoryTaskStatusSuccess || positive.RevisionID == "" { + t.Fatalf("authorized signed upload outcome=%+v", positive) + } + published, err := fixture.repositories.GetByID(repository.ID, nil) + if err != nil { + t.Fatal(err) + } + fixture.manager.repositories[repository.ID] = published + if err := fixture.content.DeleteGPGKey(fixture.ctx, repository.ID, registeredKey.ID, true); !errors.Is(err, repositoryContentService.ErrGPGKeyInUse) { + t.Fatalf("active repository key deletion error=%v", err) + } + revision, err := fixture.contentRepo.GetRevision(repository.ID, positive.RevisionID, nil) + if err != nil { + t.Fatal(err) + } + activeRepomd := filepath.Join(fixture.store.Root(), "public", repository.PublishKey, "revisions", + revision.ID, "repodata", "repomd.xml") + activeRepomdSHA, _, err := fileSHA256(activeRepomd) + if err != nil { + t.Fatal(err) + } + generated, err := fixture.content.GenerateRepositoryConfig(fixture.ctx, repository.ID, false) + if err != nil { + t.Fatal(err) + } + if !generated.GPGCheck || generated.RepoGPGCheck || len(generated.GPGKeyURLs) != 1 || + !strings.Contains(generated.Content, "gpgcheck=1\n") || + !strings.Contains(generated.Content, "gpgkey="+generated.GPGKeyURLs[0]+"\n") { + t.Fatalf("generated GPG repository config=%+v", generated) + } + assertGPGProtectedDNFInstall(t, generated.RepoID, generated.Content, "ocm-gpg-poc") + + negativeCases := []struct { + name, path, reason string + }{ + {name: "unsigned", path: unsignedRPM, reason: request.RepositoryQuarantineUnsignedRPM}, + {name: "unauthorized", path: unauthorizedRPM, reason: request.RepositoryQuarantineUnauthorizedKey}, + } + for _, test := range negativeCases { + t.Run(test.name, func(t *testing.T) { + data := readFixture(t, test.path) + task := uploadIntegrationData(t, fixture.content, fixture.ctx, repository.ID, + filepath.Base(test.path), data) + outcome := executeIntegrationTask(t, fixture.tasks, fixture.handler, task.ID) + if outcome.Status != request.RepositoryTaskStatusFailed || outcome.ErrorCode != "ALL_PACKAGES_REJECTED" { + t.Fatalf("negative upload outcome=%+v", outcome) + } + items, page, listErr := fixture.contentRepo.ListQuarantine(request.ListRepositoryQuarantineRequest{ + RepositoryID: repository.ID, TaskID: task.ID, + PageRequest: request.PageRequest{PageNumber: 1, PageSize: 20}, + }, nil) + if listErr != nil || page.Count != 1 || len(items) != 1 || items[0].ReasonCode != test.reason { + t.Fatalf("quarantine=%+v page=%+v err=%v want_reason=%s", items, page, listErr, test.reason) + } + current, getErr := fixture.repositories.GetByID(repository.ID, nil) + if getErr != nil || current.ActiveRevisionID != revision.ID { + t.Fatalf("negative upload changed active repository=%+v err=%v", current, getErr) + } + sha, _, hashErr := fileSHA256(activeRepomd) + if hashErr != nil || sha != activeRepomdSHA { + t.Fatalf("negative upload changed active repomd sha=%s want=%s err=%v", sha, activeRepomdSHA, hashErr) + } + }) + } +} + +func TestGPGProtectedRemoteSyncSnapshot(t *testing.T) { + fixture, signedRPM, _, unauthorizedRPM := newGPGIntegrationFixture(t) + upstreamRoot := t.TempDir() + copyGPGFixture(t, signedRPM, filepath.Join(upstreamRoot, filepath.Base(signedRPM))) + createRepositoryMetadata(t, upstreamRoot) + upstream := httptest.NewServer(http.FileServer(http.Dir(upstreamRoot))) + defer upstream.Close() + repository := fixture.createRepository(t, request.RepositoryTypeBaseline, upstream.URL, false) + fixture.registerAuthorizedKey(t, repository) + + task, err := fixture.content.SyncRepository(fixture.ctx, repository.ID) + if err != nil { + t.Fatal(err) + } + positive := executeIntegrationTask(t, fixture.tasks, fixture.handler, task.ID) + if positive.Status != request.RepositoryTaskStatusSuccess || positive.RevisionID == "" { + t.Fatalf("authorized signed sync outcome=%+v", positive) + } + + if err = os.RemoveAll(upstreamRoot); err != nil { + t.Fatal(err) + } + if err = os.MkdirAll(upstreamRoot, 0700); err != nil { + t.Fatal(err) + } + copyGPGFixture(t, unauthorizedRPM, filepath.Join(upstreamRoot, filepath.Base(unauthorizedRPM))) + createRepositoryMetadata(t, upstreamRoot) + failedTask, err := fixture.content.SyncRepository(fixture.ctx, repository.ID) + if err != nil { + t.Fatal(err) + } + negative := executeIntegrationTask(t, fixture.tasks, fixture.handler, failedTask.ID) + if negative.Status != request.RepositoryTaskStatusFailed || negative.ErrorCode != "SYNC_GPG_VERIFICATION_FAILED" { + t.Fatalf("unauthorized sync outcome=%+v", negative) + } + current, err := fixture.repositories.GetByID(repository.ID, nil) + if err != nil || current.ActiveRevisionID != positive.RevisionID { + t.Fatalf("failed sync changed active repository=%+v err=%v", current, err) + } +} + +func newGPGIntegrationFixture(t *testing.T) (*gpgIntegrationFixture, string, string, string) { + t.Helper() + dsn := os.Getenv("OCM_REPOSITORY_INTEGRATION_DSN") + publicKey := os.Getenv("OCM_REPOSITORY_GPG_PUBLIC_KEY") + signedRPM := os.Getenv("OCM_REPOSITORY_GPG_SIGNED_RPM") + unsignedRPM := os.Getenv("OCM_REPOSITORY_GPG_UNSIGNED_RPM") + unauthorizedRPM := os.Getenv("OCM_REPOSITORY_GPG_UNAUTHORIZED_RPM") + if dsn == "" || publicKey == "" || signedRPM == "" || unsignedRPM == "" || unauthorizedRPM == "" { + t.Skip("repository GPG integration environment is not configured") + } + db, err := database.NewDBClient(baseconfig.AccessPoint{Source: dsn}) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { db.Close() }) + if err = db.DB().AutoMigrate(&po.Project{}); err != nil { + t.Fatal(err) + } + const projectID int64 = 900010 + if err = db.DB().Exec(`INSERT IGNORE INTO projects + (id, created_at, updated_at, name, en_name, creator, organization) + VALUES (?, NOW(), NOW(), 'repository-phase10', 'repository-phase10', 'admin', 'test')`, projectID).Error; err != nil { + t.Fatal(err) + } + repositories := repositoryrepo.New(db) + tasks := repositoryTaskService.New(db) + contentRepo := repositorycontentrepo.New(db) + for _, migrate := range []func() error{repositories.SyncTables, tasks.SyncTables, contentRepo.SyncTables} { + if err = migrate(); err != nil { + t.Fatal(err) + } + } + storeRoot := t.TempDir() + t.Cleanup(func() { makeIntegrationTreeWritable(storeRoot) }) + server := httptest.NewServer(http.FileServer(http.Dir(filepath.Join(storeRoot, "public")))) + t.Cleanup(server.Close) + store, err := storage.New(appconfig.RepositoryConfig{ + Root: storeRoot, PublicBaseURL: server.URL, MaxUploadBytes: 64 << 20, + MaxChunkBytes: 1 << 20, MaxArchiveFiles: 10, MaxExpandedBytes: 128 << 20, + MaxSyncBytes: 128 << 20, MaxSyncPackages: 100, MetadataReserve: 1 << 20, MinimumFreeBytes: 1, + }) + if err != nil { + t.Fatal(err) + } + manager := &integrationRepositoryManager{repositories: make(map[string]*po.SoftwareRepository)} + ctx := entity.NewDefaultContext() + ctx.User = &ftpb.UserInfo{LoginName: "admin", Roles: []*ftpb.ModuleRole{{RoleId: request.DefaultGlobalRoleName}}} + return &gpgIntegrationFixture{ + db: db, repositories: repositories, contentRepo: contentRepo, tasks: tasks, + content: repositoryContentService.New(db, manager, tasks, store), handler: NewHandler(db, store), + manager: manager, store: store, ctx: ctx, server: server, + }, signedRPM, unsignedRPM, unauthorizedRPM +} + +func (f *gpgIntegrationFixture) createRepository(t *testing.T, repositoryType, upstream string, + allowUpload bool, +) *po.SoftwareRepository { + t.Helper() + value := &po.SoftwareRepository{ + ID: uuid.NewString(), ProjectID: 900010, SystemProduct: "OpenCloudOS", ProductVersion: "9.6", + Name: "phase10-gpg-" + strings.ToLower(repositoryType) + "-" + uuid.NewString()[:8], + Architecture: request.RepositoryArchitectureX8664, Type: repositoryType, UpstreamURL: upstream, + StorageLocation: "public/phase10", PublishKey: uuid.NewString()[:32], AllowUpload: allowUpload, + GPGPolicy: request.RepositoryGPGVerifyPackage, Enabled: true, + Status: request.RepositoryStatusEmpty, Creator: "admin", + PrimaryStorageNodeID: uuid.NewString(), + } + if err := value.SetPackageFilters(nil, nil); err != nil { + t.Fatal(err) + } + if err := f.repositories.Create(value, nil); err != nil { + t.Fatal(err) + } + f.manager.repositories[value.ID] = value + return value +} + +func (f *gpgIntegrationFixture) registerAuthorizedKey(t *testing.T, repository *po.SoftwareRepository) *request.RepositoryGPGKey { + t.Helper() + content := readFixture(t, os.Getenv("OCM_REPOSITORY_GPG_PUBLIC_KEY")) + key, err := f.content.RegisterGPGKey(f.ctx, repository.ID, content) + if err != nil { + t.Fatal(err) + } + if key.Fingerprint == "" || key.KeyID == "" || key.PublicKeyPath == "" || key.PublicKeyURL == "" { + t.Fatalf("registered GPG key=%+v", key) + } + response, err := http.Get(key.PublicKeyURL) // #nosec G107 -- URL belongs to the disposable local test server. + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK { + t.Fatalf("public GPG key HTTP status=%d", response.StatusCode) + } + rootContent, err := os.ReadFile(filepath.Join(f.store.Root(), key.PublicKeyPath)) + if err != nil || strings.Contains(string(rootContent), "PRIVATE KEY") { + t.Fatalf("stored public key contains forbidden material or cannot be read: %v", err) + } + t.Logf("gpg_key_id=%s fingerprint=%s public_http_status=200 private_material_absent=true db_metadata_only=true", + key.KeyID, key.Fingerprint) + return key +} + +func assertGPGProtectedDNFInstall(t *testing.T, repoID, repositoryConfig, packageName string) { + t.Helper() + root := t.TempDir() + configPath := filepath.Join(root, "dnf.conf") + config := fmt.Sprintf("[main]\nreposdir=%s\ncachedir=%s\npersistdir=%s\nkeepcache=0\n\n%s", + filepath.Join(root, "repos.d"), filepath.Join(root, "cache"), filepath.Join(root, "persist"), repositoryConfig) + if err := os.MkdirAll(filepath.Join(root, "repos.d"), 0700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(configPath, []byte(config), 0600); err != nil { + t.Fatal(err) + } + query := exec.Command("dnf", "-q", "-c", configPath, "--disablerepo=*", "--enablerepo="+repoID, + "repoquery", "--qf", "%{name}-%{epoch}:%{version}-%{release}.%{arch}|%{repoid}", packageName) + queryOutput, err := query.CombinedOutput() + if err != nil || !strings.Contains(string(queryOutput), "|"+repoID) { + t.Fatalf("isolated repoquery err=%v output=%s", err, queryOutput) + } + installRoot := filepath.Join(root, "installroot") + install := exec.Command("dnf", "-y", "-c", configPath, "--installroot", installRoot, "--releasever=9", + "--disablerepo=*", "--enablerepo="+repoID, "--setopt=install_weak_deps=False", "install", packageName) + installOutput, err := install.CombinedOutput() + if err != nil { + t.Fatalf("isolated GPG-protected dnf install err=%v output=%s", err, installOutput) + } + queryInstalled := exec.Command("rpm", "--root", installRoot, "-q", packageName) + if output, queryErr := queryInstalled.CombinedOutput(); queryErr != nil { + t.Fatalf("rpm -q err=%v output=%s", queryErr, output) + } + t.Logf("repo_id=%s package_source=%s install_exit_code=0", repoID, strings.TrimSpace(string(queryOutput))) +} + +func readFixture(t *testing.T, path string) []byte { + t.Helper() + value, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + return value +} + +func copyGPGFixture(t *testing.T, source, destination string) { + t.Helper() + if err := os.WriteFile(destination, readFixture(t, source), 0600); err != nil { + t.Fatal(err) + } +} + +func createRepositoryMetadata(t *testing.T, root string) { + t.Helper() + command := exec.CommandContext(context.Background(), "createrepo_c", root) + if output, err := command.CombinedOutput(); err != nil { + t.Fatalf("createrepo_c err=%v output=%s", err, output) + } +} diff --git a/manager/backend/services/footstone/repositoryworker/ingestion.go b/manager/backend/services/footstone/repositoryworker/ingestion.go new file mode 100644 index 0000000000000000000000000000000000000000..c5089e690d223fdf41e9d19303a3017b310a8547 --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/ingestion.go @@ -0,0 +1,868 @@ +package repositoryworker + +import ( + "archive/tar" + "compress/gzip" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "sort" + "strconv" + "strings" + "time" + "unicode" + + "github.com/google/uuid" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorycontentrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorydistributionrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositoryrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryTaskService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/tmsclient" +) + +type Outcome struct { + Status string + ErrorCode string + ErrorMessage string + RevisionID string +} + +type Handler struct { + db entity.SessionFactory + repositories repositoryrepo.Repository + content repositorycontentrepo.Repository + store *storage.Store + rpm RPMTool + metadata MetadataTool + syncer SyncTool + tasks repositoryTaskService.Service + distributions repositorydistributionrepo.Repository + tms tmsclient.Client + now func() time.Time +} + +type RPMInfo struct { + Name string + Epoch string + Version string + Release string + Architecture string +} + +type RPMTool interface { + Inspect(context.Context, string) (RPMInfo, error) +} + +type commandRPMTool struct{} + +func NewHandler(db *database.DBClient, store *storage.Store) *Handler { + return &Handler{ + db: db, repositories: repositoryrepo.New(db), content: repositorycontentrepo.New(db), + store: store, rpm: commandRPMTool{}, metadata: commandMetadataTool{}, syncer: commandSyncTool{}, + tasks: repositoryTaskService.New(db), + distributions: repositorydistributionrepo.New(db), + tms: tmsclient.New(store.Config().TMSBaseURL, store.Config().TMSUserName), + now: func() time.Time { return time.Now().UTC() }, + } +} + +func (h *Handler) Handle(ctx context.Context, task *po.RepositoryTask) Outcome { + if task == nil { + return failed("INVALID_TASK", "repository task is required") + } + switch task.Type { + case request.RepositoryTaskTypeSync: + return h.handleSync(ctx, task) + case request.RepositoryTaskTypeUpload: + return h.handleUpload(ctx, task) + case request.RepositoryTaskTypeDeletePackage: + return h.handleDeletePackage(ctx, task) + case request.RepositoryTaskTypeDistribute: + return h.handleDistribution(ctx, task) + default: + return failed("UNSUPPORTED_TASK_TYPE", fmt.Sprintf("worker does not handle %s", task.Type)) + } +} + +func (h *Handler) handleUpload(ctx context.Context, task *po.RepositoryTask) Outcome { + var payload struct { + UploadID string `json:"upload_id"` + } + if err := json.Unmarshal([]byte(task.PayloadJSON), &payload); err != nil || uuid.Validate(payload.UploadID) != nil { + return failed("INVALID_UPLOAD_PAYLOAD", "upload task payload is invalid") + } + upload, err := h.content.GetUpload(payload.UploadID, false, nil) + if err != nil { + return failed("UPLOAD_NOT_FOUND", err.Error()) + } + repository, err := h.repositories.GetByID(task.RepositoryID, nil) + if err != nil { + return failed("REPOSITORY_NOT_FOUND", err.Error()) + } + if upload.RepositoryID != repository.ID || upload.ProjectID != repository.ProjectID { + return failed("UPLOAD_REPOSITORY_MISMATCH", "upload does not belong to the task repository") + } + verifier, err := h.newSignatureSession(ctx, task, repository) + if err != nil { + message := err.Error() + if persistErr := h.persistUploadResult(upload, nil, request.RepositoryUploadStatusFailed, + "GPG_KEY_UNAVAILABLE", message); persistErr != nil { + return failed("UPLOAD_STATE_FAILED", persistErr.Error()) + } + return failed("GPG_KEY_UNAVAILABLE", message) + } + if verifier != nil { + defer verifier.Close() + } + + candidates, archiveErr := h.collectCandidates(ctx, task, upload) + if archiveErr != nil { + item, quarantineErr := h.quarantineFile(task, upload, upload.RelativePath, + upload.FileName, request.RepositoryQuarantineDangerousArchive, archiveErr.Error(), "") + if quarantineErr != nil { + return failed("QUARANTINE_FAILED", quarantineErr.Error()) + } + if err = h.persistUploadFailure(upload, item, "DANGEROUS_ARCHIVE", archiveErr.Error()); err != nil { + return failed("UPLOAD_STATE_FAILED", err.Error()) + } + h.cleanupUploadStaging(upload, task) + return failed("DANGEROUS_ARCHIVE", archiveErr.Error()) + } + return h.buildUploadRevision(ctx, task, upload, repository, candidates, verifier) +} + +type candidate struct { + OriginalName string + RelativePath string + Info RPMInfo + SHA256 string + Size int64 + NEVRA string + RejectCode string + RejectReason string + SignatureStatus string + SignerKeyID string + SignatureMessage string +} + +func (h *Handler) collectCandidates(ctx context.Context, task *po.RepositoryTask, + upload *po.RepositoryUpload, +) ([]candidate, error) { + if upload.Kind == request.RepositoryUploadKindRPM { + return []candidate{{OriginalName: upload.FileName, RelativePath: upload.RelativePath}}, nil + } + if upload.Kind != request.RepositoryUploadKindTarGZ { + return nil, fmt.Errorf("unsupported upload kind %q", upload.Kind) + } + archivePath, err := h.store.Resolve(upload.RelativePath) + if err != nil { + return nil, err + } + file, err := os.Open(archivePath) + if err != nil { + return nil, err + } + defer file.Close() + gz, err := gzip.NewReader(file) + if err != nil { + return nil, fmt.Errorf("invalid gzip stream: %w", err) + } + defer gz.Close() + + extractRelative := filepath.ToSlash(filepath.Join("staging", "tasks", task.ID, "extracted")) + extractRoot, err := h.store.Resolve(extractRelative) + if err != nil { + return nil, err + } + if err = os.RemoveAll(extractRoot); err != nil { + return nil, err + } + if err = os.MkdirAll(extractRoot, 0700); err != nil { + return nil, err + } + tr := tar.NewReader(gz) + result := make([]candidate, 0) + var expanded int64 + for { + header, nextErr := tr.Next() + if errors.Is(nextErr, io.EOF) { + break + } + if nextErr != nil { + return nil, fmt.Errorf("read tar entry: %w", nextErr) + } + if ctx.Err() != nil { + return nil, ctx.Err() + } + name := filepath.ToSlash(header.Name) + clean := filepath.Clean(filepath.FromSlash(name)) + baseName := filepath.Base(clean) + if len(baseName) == 0 || len(baseName) > 255 || strings.IndexFunc(baseName, unicode.IsControl) >= 0 { + return nil, fmt.Errorf("archive contains unsafe file name %q", header.Name) + } + if filepath.IsAbs(clean) || clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) { + return nil, fmt.Errorf("archive contains path traversal entry %q", header.Name) + } + switch header.Typeflag { + case tar.TypeDir: + continue + case tar.TypeReg, tar.TypeRegA: + default: + return nil, fmt.Errorf("archive contains non-regular entry %q", header.Name) + } + if !strings.HasSuffix(strings.ToLower(clean), ".rpm") { + return nil, fmt.Errorf("archive contains non-RPM file %q", header.Name) + } + if header.Size < 0 || header.Size > h.store.Config().MaxExpandedBytes-expanded { + return nil, fmt.Errorf("archive expanded size exceeds %d bytes", h.store.Config().MaxExpandedBytes) + } + if len(result) >= h.store.Config().MaxArchiveFiles { + return nil, fmt.Errorf("archive file count exceeds %d", h.store.Config().MaxArchiveFiles) + } + expanded += header.Size + destination := filepath.Join(extractRoot, fmt.Sprintf("%06d.rpm", len(result)+1)) + out, createErr := os.OpenFile(destination, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) + if createErr != nil { + return nil, createErr + } + written, copyErr := io.CopyN(out, tr, header.Size) + closeErr := out.Close() + if copyErr != nil || written != header.Size || closeErr != nil { + return nil, fmt.Errorf("extract archive entry %q: copy=%v close=%v", header.Name, copyErr, closeErr) + } + result = append(result, candidate{ + OriginalName: baseName, + RelativePath: filepath.ToSlash(filepath.Join(extractRelative, filepath.Base(destination))), + }) + } + if len(result) == 0 { + return nil, fmt.Errorf("archive contains no RPM files") + } + return result, nil +} + +func (h *Handler) buildUploadRevision(ctx context.Context, task *po.RepositoryTask, upload *po.RepositoryUpload, + repository *po.SoftwareRepository, candidates []candidate, verifier *signatureSession, +) Outcome { + parent, err := h.activeRevision(repository) + if err != nil { + return failed("REVISION_LOOKUP_FAILED", err.Error()) + } + basePackages := []po.RepositoryPackage{} + if parent != nil { + basePackages, err = h.content.PackagesByRevision(parent.ID, nil) + if err != nil { + return failed("PACKAGE_INVENTORY_FAILED", err.Error()) + } + } + existing := make(map[string]po.RepositoryPackage, len(basePackages)) + for _, pkg := range basePackages { + existing[pkg.NEVRA] = pkg + } + + accepted := make([]candidate, 0, len(candidates)) + rejected := make([]candidate, 0) + duplicates := 0 + for index := range candidates { + item := &candidates[index] + path, resolveErr := h.store.Resolve(item.RelativePath) + if resolveErr != nil { + item.RejectCode, item.RejectReason = request.RepositoryQuarantineInvalidRPM, resolveErr.Error() + rejected = append(rejected, *item) + continue + } + item.SHA256, item.Size, err = fileSHA256(path) + if err != nil { + item.RejectCode, item.RejectReason = request.RepositoryQuarantineDamagedRPM, err.Error() + rejected = append(rejected, *item) + continue + } + item.Info, err = h.rpm.Inspect(ctx, path) + if err != nil { + item.RejectCode, item.RejectReason = request.RepositoryQuarantineDamagedRPM, err.Error() + rejected = append(rejected, *item) + continue + } + if verifyErr := evaluateCandidateSignature(ctx, verifier, path, item); verifyErr != nil { + var signatureErr *signatureVerificationError + if errors.As(verifyErr, &signatureErr) { + item.RejectCode, item.RejectReason = signatureErr.Code, signatureErr.Message + } else { + item.RejectCode, item.RejectReason = request.RepositoryQuarantineBadSignature, verifyErr.Error() + } + rejected = append(rejected, *item) + continue + } + if !architectureAllowed(repository.Architecture, item.Info.Architecture) { + item.RejectCode = request.RepositoryQuarantineWrongArch + item.RejectReason = fmt.Sprintf("package architecture %s is not allowed in %s repository", + item.Info.Architecture, repository.Architecture) + rejected = append(rejected, *item) + continue + } + item.NEVRA = formatNEVRA(item.Info) + if previous, exists := existing[item.NEVRA]; exists { + if previous.SHA256 == item.SHA256 { + duplicates++ + continue + } + item.RejectCode = request.RepositoryQuarantineNEVRAConflict + item.RejectReason = "the same NEVRA already exists with a different SHA-256" + rejected = append(rejected, *item) + continue + } + existing[item.NEVRA] = po.RepositoryPackage{NEVRA: item.NEVRA, SHA256: item.SHA256} + accepted = append(accepted, *item) + } + + quarantineItems := make([]*po.RepositoryQuarantineItem, 0, len(rejected)) + for _, item := range rejected { + q, quarantineErr := h.quarantineFile(task, upload, item.RelativePath, item.OriginalName, + item.RejectCode, item.RejectReason, item.NEVRA) + if quarantineErr != nil { + return failed("QUARANTINE_FAILED", quarantineErr.Error()) + } + quarantineItems = append(quarantineItems, q) + } + if len(accepted) == 0 { + status := request.RepositoryUploadStatusCompleted + code, message := "", "" + outcome := Outcome{Status: request.RepositoryTaskStatusSuccess} + if len(rejected) > 0 { + status = request.RepositoryUploadStatusFailed + code = "ALL_PACKAGES_REJECTED" + message = fmt.Sprintf("all %d RPM packages were rejected; reasons=%s", + len(rejected), rejectionSummary(rejected)) + outcome = failed(code, message) + } else if duplicates > 0 { + message = fmt.Sprintf("all %d RPM packages already exist with identical content", duplicates) + } + if err = h.persistUploadResult(upload, quarantineItems, status, code, message); err != nil { + return failed("UPLOAD_STATE_FAILED", err.Error()) + } + h.cleanupUploadStaging(upload, task) + return outcome + } + + revision, packages, finalRelative, buildErr := h.materializeRevision(task, repository, parent, basePackages, accepted) + if buildErr != nil { + return failed("REVISION_BUILD_FAILED", buildErr.Error()) + } + status := request.RepositoryUploadStatusCompleted + outcome := Outcome{Status: request.RepositoryTaskStatusSuccess} + if len(rejected) > 0 { + outcome.Status = request.RepositoryTaskStatusPartialSuccess + outcome.ErrorCode = "SOME_PACKAGES_REJECTED" + outcome.ErrorMessage = fmt.Sprintf("accepted=%d rejected=%d reasons=%s", + len(accepted), len(rejected), rejectionSummary(rejected)) + } + outcome.RevisionID = revision.ID + _, err = h.publishRevision(ctx, task, repository, revision, packages, func(tx entity.Session) error { + for _, item := range quarantineItems { + if createErr := h.content.CreateQuarantine(item, tx); createErr != nil { + return createErr + } + } + upload.Status, upload.ErrorCode, upload.ErrorMessage = status, outcome.ErrorCode, outcome.ErrorMessage + return h.content.UpdateUpload(upload, tx) + }) + if err != nil { + h.moveRevisionToTrash(finalRelative, revision.ID) + return failed("REVISION_PUBLISH_FAILED", err.Error()) + } + h.cleanupUploadStaging(upload, task) + return outcome +} + +func rejectionSummary(rejected []candidate) string { + counts := make(map[string]int) + for _, item := range rejected { + code := strings.TrimSpace(item.RejectCode) + if code == "" { + code = "UNKNOWN" + } + counts[code]++ + } + parts := make([]string, 0, len(counts)) + for code, count := range counts { + parts = append(parts, fmt.Sprintf("%s:%d", code, count)) + } + sort.Strings(parts) + return strings.Join(parts, ",") +} + +func (h *Handler) materializeRevision(task *po.RepositoryTask, repository *po.SoftwareRepository, + parent *po.RepositoryRevision, base []po.RepositoryPackage, accepted []candidate, +) (*po.RepositoryRevision, []po.RepositoryPackage, string, error) { + revisionID := uuid.NewString() + stagingRelative := filepath.ToSlash(filepath.Join("staging", "revisions", task.ID, revisionID)) + finalRelative := filepath.ToSlash(filepath.Join("revisions-private", repository.ID, revisionID)) + stagingPath, err := h.store.Resolve(stagingRelative) + if err != nil { + return nil, nil, "", err + } + finalPath, err := h.store.Resolve(finalRelative) + if err != nil { + return nil, nil, "", err + } + taskStaging, err := h.store.Resolve(filepath.ToSlash(filepath.Join("staging", "revisions", task.ID))) + if err != nil { + return nil, nil, "", err + } + if err = os.RemoveAll(taskStaging); err != nil { + return nil, nil, "", err + } + if err = h.store.EnsureFreeSpace(totalPackageSize(base) + totalCandidateSize(accepted)); err != nil { + return nil, nil, "", err + } + if err = os.MkdirAll(stagingPath, 0750); err != nil { + return nil, nil, "", err + } + packages := make([]po.RepositoryPackage, 0, len(base)+len(accepted)) + for _, old := range base { + source, resolveErr := h.store.Resolve(old.RelativePath) + if resolveErr != nil { + return nil, nil, "", resolveErr + } + name := safePackageFileName(old.SHA256, old.FileName) + destination := filepath.Join(stagingPath, name) + if err = linkOrCopy(source, destination); err != nil { + return nil, nil, "", err + } + old.ID = uuid.NewString() + old.RevisionID = revisionID + old.RelativePath = filepath.ToSlash(filepath.Join(finalRelative, name)) + old.CreatedAt = time.Time{} + packages = append(packages, old) + } + for _, item := range accepted { + source, resolveErr := h.store.Resolve(item.RelativePath) + if resolveErr != nil { + return nil, nil, "", resolveErr + } + name := safePackageFileName(item.SHA256, item.OriginalName) + destination := filepath.Join(stagingPath, name) + if err = linkOrCopy(source, destination); err != nil { + return nil, nil, "", err + } + packages = append(packages, po.RepositoryPackage{ + ID: uuid.NewString(), RepositoryID: repository.ID, RevisionID: revisionID, + Name: item.Info.Name, Epoch: normalizeEpoch(item.Info.Epoch), Version: item.Info.Version, + Release: item.Info.Release, Architecture: item.Info.Architecture, NEVRA: item.NEVRA, + FileName: item.OriginalName, RelativePath: filepath.ToSlash(filepath.Join(finalRelative, name)), + SizeBytes: item.Size, SHA256: item.SHA256, Source: request.RepositoryPackageSourceUpload, + SourceUploadID: taskPayloadUploadID(task.PayloadJSON), + SignatureStatus: item.SignatureStatus, SignerKeyID: item.SignerKeyID, + SignatureMessage: item.SignatureMessage, + }) + } + if err = os.MkdirAll(filepath.Dir(finalPath), 0750); err != nil { + return nil, nil, "", err + } + if err = os.Rename(stagingPath, finalPath); err != nil { + return nil, nil, "", fmt.Errorf("finalize private revision: %w", err) + } + for _, pkg := range packages { + path, resolveErr := h.store.Resolve(pkg.RelativePath) + if resolveErr == nil { + _ = os.Chmod(path, 0440) + } + } + manifest, total := packageManifest(packages) + parentID := "" + if parent != nil { + parentID = parent.ID + } + revision := &po.RepositoryRevision{ + ID: revisionID, RepositoryID: repository.ID, StorageNodeID: task.StorageNodeID, + ParentRevisionID: parentID, TaskID: task.ID, + Kind: request.RepositoryRevisionKindUpload, Status: request.RepositoryRevisionStatusReady, + PackageCount: len(packages), TotalBytes: total, ManifestSHA256: manifest, + RelativePath: finalRelative, CreatedBy: task.RequestedBy, + } + return revision, packages, finalRelative, nil +} + +func (h *Handler) handleDeletePackage(ctx context.Context, task *po.RepositoryTask) Outcome { + payload, err := decodeDeletePackagePayload(task.PayloadJSON) + if err != nil { + return failed("INVALID_DELETE_PAYLOAD", "delete package task payload is invalid") + } + repository, err := h.repositories.GetByID(task.RepositoryID, nil) + if err != nil { + return failed("REPOSITORY_NOT_FOUND", err.Error()) + } + parent, err := h.activeRevision(repository) + if err != nil || parent == nil { + return failed("REVISION_NOT_FOUND", "repository has no private revision") + } + if payload.RevisionID != "" && payload.RevisionID != parent.ID { + return failed("REVISION_CHANGED", "active repository revision changed after package deletion preview") + } + base, err := h.content.PackagesByRevision(parent.ID, nil) + if err != nil { + return failed("PACKAGE_INVENTORY_FAILED", err.Error()) + } + deleting := make(map[string]struct{}, len(payload.NEVRAs)) + for _, nevra := range payload.NEVRAs { + deleting[nevra] = struct{}{} + } + remaining := make([]po.RepositoryPackage, 0, len(base)) + found := make(map[string]struct{}, len(deleting)) + for _, pkg := range base { + if _, ok := deleting[pkg.NEVRA]; ok { + found[pkg.NEVRA] = struct{}{} + continue + } + remaining = append(remaining, pkg) + } + if len(found) != len(deleting) { + return failed("PACKAGE_NOT_FOUND", "one or more selected NEVRAs do not exist") + } + revision, packages, finalRelative, err := h.materializeDeleteRevision(task, repository, parent, remaining) + if err != nil { + return failed("REVISION_BUILD_FAILED", err.Error()) + } + if _, err = h.publishRevision(ctx, task, repository, revision, packages, func(tx entity.Session) error { + return h.tasks.RecordPackageDeleteResults(task, payload.NEVRAs, tx) + }); err != nil { + h.moveRevisionToTrash(finalRelative, revision.ID) + return failed("REVISION_PUBLISH_FAILED", err.Error()) + } + return Outcome{Status: request.RepositoryTaskStatusSuccess, RevisionID: revision.ID} +} + +func (h *Handler) materializeDeleteRevision(task *po.RepositoryTask, repository *po.SoftwareRepository, + parent *po.RepositoryRevision, remaining []po.RepositoryPackage, +) (*po.RepositoryRevision, []po.RepositoryPackage, string, error) { + revisionID := uuid.NewString() + stagingRelative := filepath.ToSlash(filepath.Join("staging", "revisions", task.ID, revisionID)) + finalRelative := filepath.ToSlash(filepath.Join("revisions-private", repository.ID, revisionID)) + staging, err := h.store.Resolve(stagingRelative) + if err != nil { + return nil, nil, "", err + } + final, err := h.store.Resolve(finalRelative) + if err != nil { + return nil, nil, "", err + } + taskStaging, err := h.store.Resolve(filepath.ToSlash(filepath.Join("staging", "revisions", task.ID))) + if err != nil { + return nil, nil, "", err + } + if err = os.RemoveAll(taskStaging); err != nil { + return nil, nil, "", err + } + if err = h.store.EnsureFreeSpace(totalPackageSize(remaining)); err != nil { + return nil, nil, "", err + } + if err = os.MkdirAll(staging, 0750); err != nil { + return nil, nil, "", err + } + packages := make([]po.RepositoryPackage, 0, len(remaining)) + for _, old := range remaining { + source, resolveErr := h.store.Resolve(old.RelativePath) + if resolveErr != nil { + return nil, nil, "", resolveErr + } + name := safePackageFileName(old.SHA256, old.FileName) + if err = linkOrCopy(source, filepath.Join(staging, name)); err != nil { + return nil, nil, "", err + } + old.ID, old.RevisionID = uuid.NewString(), revisionID + old.RelativePath = filepath.ToSlash(filepath.Join(finalRelative, name)) + old.CreatedAt = time.Time{} + packages = append(packages, old) + } + if err = os.MkdirAll(filepath.Dir(final), 0750); err != nil { + return nil, nil, "", err + } + if err = os.Rename(staging, final); err != nil { + return nil, nil, "", err + } + manifest, total := packageManifest(packages) + revision := &po.RepositoryRevision{ + ID: revisionID, RepositoryID: repository.ID, StorageNodeID: task.StorageNodeID, + ParentRevisionID: parent.ID, TaskID: task.ID, + Kind: request.RepositoryRevisionKindDeletePackage, Status: request.RepositoryRevisionStatusReady, + PackageCount: len(packages), TotalBytes: total, ManifestSHA256: manifest, + RelativePath: finalRelative, CreatedBy: task.RequestedBy, + } + return revision, packages, finalRelative, nil +} + +func (h *Handler) persistUploadResult(upload *po.RepositoryUpload, + quarantine []*po.RepositoryQuarantineItem, status, code, message string, +) (err error) { + tx, maker := entity.NewSessionMaker(nil, h.db) + defer maker.Close(&err) + for _, item := range quarantine { + if err = h.content.CreateQuarantine(item, tx); err != nil { + return err + } + } + upload.Status, upload.ErrorCode, upload.ErrorMessage = status, code, message + return h.content.UpdateUpload(upload, tx) +} + +func (h *Handler) persistUploadFailure(upload *po.RepositoryUpload, + item *po.RepositoryQuarantineItem, code, message string, +) error { + return h.persistUploadResult(upload, []*po.RepositoryQuarantineItem{item}, + request.RepositoryUploadStatusFailed, code, message) +} + +func (h *Handler) FinalizeUploadOutcome(task *po.RepositoryTask, outcome Outcome) error { + if task == nil || task.Type != request.RepositoryTaskTypeUpload { + return nil + } + switch outcome.Status { + case request.RepositoryTaskStatusSuccess, request.RepositoryTaskStatusPartialSuccess: + return nil + } + var payload struct { + UploadID string `json:"upload_id"` + } + if err := json.Unmarshal([]byte(task.PayloadJSON), &payload); err != nil || + uuid.Validate(payload.UploadID) != nil { + return nil + } + upload, err := h.content.GetUpload(payload.UploadID, true, nil) + if err != nil { + return nil + } + if upload.RepositoryID != task.RepositoryID { + return nil + } + if upload.TaskID != "" && upload.TaskID != task.ID && upload.TaskID != task.RootTaskID { + return nil + } + switch upload.Status { + case request.RepositoryUploadStatusReceiving, request.RepositoryUploadStatusReady, + request.RepositoryUploadStatusProcessing: + default: + return nil + } + code := strings.TrimSpace(outcome.ErrorCode) + if code == "" { + code = "TASK_FAILED" + } + upload.Status = request.RepositoryUploadStatusFailed + upload.ErrorCode = truncateText(code, 64) + upload.ErrorMessage = truncateText(outcome.ErrorMessage, 2048) + return h.content.UpdateUpload(upload, nil) +} + +func (h *Handler) quarantineFile(task *po.RepositoryTask, upload *po.RepositoryUpload, sourceRelative, + originalName, code, message, nevra string, +) (*po.RepositoryQuarantineItem, error) { + source, err := h.store.Resolve(sourceRelative) + if err != nil { + return nil, err + } + digest, size, err := fileSHA256(source) + if err != nil { + return nil, err + } + safeName := safePackageFileName(digest, originalName) + destinationRelative := filepath.ToSlash(filepath.Join("quarantine", upload.RepositoryID, task.ID, safeName)) + destination, err := h.store.Resolve(destinationRelative) + if err != nil { + return nil, err + } + if err = os.MkdirAll(filepath.Dir(destination), 0750); err != nil { + return nil, err + } + if err = os.Rename(source, destination); err != nil { + if err = copyFile(source, destination); err != nil { + return nil, err + } + } + _ = os.Chmod(destination, 0440) + return &po.RepositoryQuarantineItem{ + ID: uuid.NewString(), RepositoryID: upload.RepositoryID, TaskID: task.ID, UploadID: upload.ID, + OriginalName: originalName, RelativePath: destinationRelative, SizeBytes: size, SHA256: digest, + ReasonCode: code, ReasonMessage: message, DetectedNEVRA: nevra, + }, nil +} + +func (h *Handler) moveRevisionToTrash(relative, revisionID string) { + path, err := h.store.Resolve(relative) + if err != nil { + return + } + destination, err := h.store.Resolve(filepath.ToSlash(filepath.Join("trash", "failed-revisions", revisionID))) + if err == nil { + _ = os.MkdirAll(filepath.Dir(destination), 0750) + _ = os.Rename(path, destination) + } +} + +func (h *Handler) cleanupUploadStaging(upload *po.RepositoryUpload, task *po.RepositoryTask) { + if upload.RelativePath != "" { + _, _ = h.store.MoveToTrash(upload.RelativePath, "processed-uploads", upload.ID) + } + for _, relative := range []string{ + filepath.ToSlash(filepath.Join("staging", "tasks", task.ID)), + filepath.ToSlash(filepath.Join("staging", "revisions", task.ID)), + } { + path, err := h.store.Resolve(relative) + if err == nil { + _ = os.RemoveAll(path) + } + } +} + +func (commandRPMTool) Inspect(ctx context.Context, path string) (RPMInfo, error) { + verify := exec.CommandContext(ctx, "rpm", "-K", "--nosignature", path) + if output, err := verify.CombinedOutput(); err != nil { + return RPMInfo{}, fmt.Errorf("rpm integrity verification failed: %s", truncateOutput(output)) + } + format := "%{NAME}\\t%{EPOCHNUM}\\t%{VERSION}\\t%{RELEASE}\\t%{ARCH}\\n" + query := exec.CommandContext(ctx, "rpm", "-qp", "--qf", format, path) + output, err := query.Output() + if err != nil { + return RPMInfo{}, fmt.Errorf("rpm header query failed: %w", err) + } + fields := strings.Split(strings.TrimSpace(string(output)), "\t") + if len(fields) != 5 { + return RPMInfo{}, fmt.Errorf("rpm header query returned %d fields", len(fields)) + } + limits := []int{255, 20, 255, 255, 64} + for index, field := range fields { + if field == "" || len(field) > limits[index] || strings.ContainsAny(field, "\r\n\t/") || + strings.IndexFunc(field, unicode.IsControl) >= 0 { + return RPMInfo{}, fmt.Errorf("rpm header contains an unsafe field") + } + } + return RPMInfo{Name: fields[0], Epoch: normalizeEpoch(fields[1]), Version: fields[2], + Release: fields[3], Architecture: strings.ToLower(fields[4])}, nil +} + +func architectureAllowed(repositoryArch, packageArch string) bool { + return packageArch == "noarch" || packageArch == repositoryArch +} + +func formatNEVRA(info RPMInfo) string { + return fmt.Sprintf("%s-%s:%s-%s.%s", info.Name, normalizeEpoch(info.Epoch), info.Version, + info.Release, info.Architecture) +} + +func normalizeEpoch(value string) string { + value = strings.TrimSpace(value) + if value == "" || value == "(none)" || value == "(null)" { + return "0" + } + if _, err := strconv.ParseUint(value, 10, 64); err != nil { + return "0" + } + return value +} + +func packageManifest(packages []po.RepositoryPackage) (string, int64) { + lines := make([]string, 0, len(packages)) + var total int64 + for _, pkg := range packages { + lines = append(lines, fmt.Sprintf("%s\t%s\t%d", pkg.NEVRA, pkg.SHA256, pkg.SizeBytes)) + total += pkg.SizeBytes + } + sort.Strings(lines) + digest := sha256.Sum256([]byte(strings.Join(lines, "\n") + "\n")) + return hex.EncodeToString(digest[:]), total +} + +func fileSHA256(path string) (string, int64, error) { + file, err := os.Open(path) + if err != nil { + return "", 0, err + } + defer file.Close() + hash := sha256.New() + size, err := io.Copy(hash, file) + if err != nil { + return "", 0, err + } + return hex.EncodeToString(hash.Sum(nil)), size, nil +} + +func safePackageFileName(digest, original string) string { + name := filepath.Base(original) + if !strings.HasSuffix(strings.ToLower(name), ".rpm") { + name += ".rpm" + } + return digest[:16] + "-" + name +} + +func linkOrCopy(source, destination string) error { + if err := os.Link(source, destination); err == nil { + return nil + } + return copyFile(source, destination) +} + +func copyFile(source, destination string) error { + in, err := os.Open(source) + if err != nil { + return err + } + defer in.Close() + out, err := os.OpenFile(destination, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0440) + if err != nil { + return err + } + _, copyErr := io.Copy(out, in) + closeErr := out.Close() + if copyErr != nil { + return copyErr + } + return closeErr +} + +func totalCandidateSize(values []candidate) int64 { + var result int64 + for _, value := range values { + result += value.Size + } + return result +} + +func totalPackageSize(values []po.RepositoryPackage) int64 { + var result int64 + for _, value := range values { + result += value.SizeBytes + } + return result +} + +func taskPayloadUploadID(payload string) string { + var value struct { + UploadID string `json:"upload_id"` + } + _ = json.Unmarshal([]byte(payload), &value) + return value.UploadID +} + +func truncateOutput(value []byte) string { + text := strings.TrimSpace(string(value)) + if len(text) > 512 { + return text[:512] + } + return text +} + +func failed(code, message string) Outcome { + return Outcome{Status: request.RepositoryTaskStatusFailed, ErrorCode: code, ErrorMessage: message} +} diff --git a/manager/backend/services/footstone/repositoryworker/ingestion_integration_test.go b/manager/backend/services/footstone/repositoryworker/ingestion_integration_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d638bbab5017ff7ead2b9670af6cbebc4cf47f15 --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/ingestion_integration_test.go @@ -0,0 +1,339 @@ +package repositoryworker + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "crypto/sha256" + "encoding/hex" + "os" + "path/filepath" + "testing" + "time" + + "github.com/google/uuid" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + baseconfig "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + ftpb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/footstone" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorycontentrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositoryrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryContentService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryTaskService" + appconfig "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" +) + +func TestPrivateIngestionMySQL(t *testing.T) { + dsn := os.Getenv("OCM_REPOSITORY_INTEGRATION_DSN") + firstRPM := os.Getenv("OCM_REPOSITORY_TEST_RPM_ONE") + secondRPM := os.Getenv("OCM_REPOSITORY_TEST_RPM_TWO") + x86RPM := os.Getenv("OCM_REPOSITORY_TEST_RPM_X86") + if dsn == "" || firstRPM == "" || secondRPM == "" || x86RPM == "" { + t.Skip("repository MySQL integration environment is not configured") + } + db, err := database.NewDBClient(baseconfig.AccessPoint{Source: dsn}) + if err != nil { + t.Fatal(err) + } + defer db.Close() + if err = db.DB().AutoMigrate(&po.Project{}); err != nil { + t.Fatal(err) + } + if err = db.DB().Exec(`INSERT IGNORE INTO projects + (id, created_at, updated_at, name, en_name, creator, organization) + VALUES (900003, NOW(), NOW(), 'repository-phase3', 'repository-phase3', 'admin', 'test')`).Error; err != nil { + t.Fatal(err) + } + + repositories := repositoryrepo.New(db) + tasks := repositoryTaskService.New(db) + contentRepo := repositorycontentrepo.New(db) + if err = repositories.SyncTables(); err != nil { + t.Fatal(err) + } + if err = tasks.SyncTables(); err != nil { + t.Fatal(err) + } + if err = contentRepo.SyncTables(); err != nil { + t.Fatal(err) + } + repository := &po.SoftwareRepository{ + ID: uuid.NewString(), ProjectID: 900003, SystemProduct: "OpenCloudOS", ProductVersion: "9.6", + Name: "phase3-private-" + uuid.NewString()[:8], Architecture: request.RepositoryArchitectureX8664, + Type: request.RepositoryTypePrivate, StorageLocation: "public/phase3", PublishKey: uuid.NewString()[:32], + AllowUpload: true, GPGPolicy: request.RepositoryGPGDisabled, Enabled: true, + PrimaryStorageNodeID: uuid.NewString(), + Status: request.RepositoryStatusEmpty, Creator: "admin", + } + if err = repository.SetPackageFilters(nil, nil); err != nil { + t.Fatal(err) + } + if err = repositories.Create(repository, nil); err != nil { + t.Fatal(err) + } + armRepository := *repository + armRepository.ID = uuid.NewString() + armRepository.Name = "phase3-arm-private-" + uuid.NewString()[:8] + armRepository.Architecture = request.RepositoryArchitectureARM64 + armRepository.PublishKey = uuid.NewString()[:32] + if err = repositories.Create(&armRepository, nil); err != nil { + t.Fatal(err) + } + + storeRoot := t.TempDir() + t.Cleanup(func() { makeIntegrationTreeWritable(storeRoot) }) + store, err := storage.New(appconfig.RepositoryConfig{ + Root: storeRoot, MaxUploadBytes: 64 << 20, MaxChunkBytes: 1 << 20, + MaxArchiveFiles: 10, MaxExpandedBytes: 128 << 20, + }) + if err != nil { + t.Fatal(err) + } + manager := &integrationRepositoryManager{repositories: map[string]*po.SoftwareRepository{ + repository.ID: repository, armRepository.ID: &armRepository, + }} + content := repositoryContentService.New(db, manager, tasks, store) + handler := NewHandler(db, store) + ctx := entity.NewDefaultContext() + ctx.User = &ftpb.UserInfo{LoginName: "admin", Roles: []*ftpb.ModuleRole{{RoleId: request.DefaultGlobalRoleName}}} + + firstData, err := os.ReadFile(firstRPM) + if err != nil { + t.Fatal(err) + } + firstTask := uploadIntegrationData(t, content, ctx, repository.ID, filepath.Base(firstRPM), firstData) + firstOutcome := executeIntegrationTask(t, tasks, handler, firstTask.ID) + if firstOutcome.Status != request.RepositoryTaskStatusSuccess { + t.Fatalf("first upload outcome=%+v", firstOutcome) + } + firstRevision, err := contentRepo.LatestReadyRevision(repository.ID, nil) + if err != nil || firstRevision == nil || firstRevision.PackageCount != 1 || + firstRevision.Status != request.RepositoryRevisionStatusPublished || firstRevision.RepomdSHA256 == "" || + firstRevision.SnapshotManifestSHA256 == "" { + t.Fatalf("first revision=%+v err=%v", firstRevision, err) + } + assertIntegrationTaskRevision(t, tasks, ctx, firstTask.ID, firstRevision.ID) + storedRepository, err := repositories.GetByID(repository.ID, nil) + if err != nil || storedRepository.Status != request.RepositoryStatusReady || + storedRepository.ActiveRevisionID != firstRevision.ID { + t.Fatalf("repository status=%+v err=%v", storedRepository, err) + } + current := filepath.Join(store.Root(), "public", repository.PublishKey, "current") + if target, readErr := os.Readlink(current); readErr != nil || + filepath.ToSlash(target) != "revisions/"+firstRevision.ID { + t.Fatalf("current target=%q err=%v", target, readErr) + } + if _, statErr := os.Stat(filepath.Join(current, "repodata", "repomd.xml")); statErr != nil { + t.Fatalf("published repomd.xml: %v", statErr) + } + t.Logf("private publish revision=%s repomd=%s manifest=%s current=revisions/%s", + firstRevision.ID, firstRevision.RepomdSHA256, firstRevision.SnapshotManifestSHA256, firstRevision.ID) + firstPackages, err := contentRepo.PackagesByRevision(firstRevision.ID, nil) + if err != nil || len(firstPackages) != 1 { + t.Fatalf("first packages=%+v err=%v", firstPackages, err) + } + + secondData, err := os.ReadFile(secondRPM) + if err != nil { + t.Fatal(err) + } + archive := integrationTarGZ(t, map[string][]byte{ + filepath.Base(secondRPM): secondData, + "damaged.rpm": []byte("not-an-rpm"), + }) + secondTask := uploadIntegrationData(t, content, ctx, repository.ID, "phase3-batch.tar.gz", archive) + secondOutcome := executeIntegrationTask(t, tasks, handler, secondTask.ID) + if secondOutcome.Status != request.RepositoryTaskStatusPartialSuccess { + t.Fatalf("tar upload outcome=%+v", secondOutcome) + } + secondRevision, err := contentRepo.LatestReadyRevision(repository.ID, nil) + if err != nil || secondRevision == nil || secondRevision.PackageCount != 2 || secondRevision.ID == firstRevision.ID { + t.Fatalf("second revision=%+v err=%v", secondRevision, err) + } + quarantine, page, err := contentRepo.ListQuarantine(request.ListRepositoryQuarantineRequest{ + RepositoryID: repository.ID, TaskID: secondTask.ID, + PageRequest: request.PageRequest{PageNumber: 1, PageSize: 20}, + }, nil) + if err != nil || page.Count != 1 || len(quarantine) != 1 || + quarantine[0].ReasonCode != request.RepositoryQuarantineDamagedRPM { + t.Fatalf("quarantine=%+v page=%+v err=%v", quarantine, page, err) + } + + dangerousArchive := integrationTarGZ(t, map[string][]byte{"../escape.rpm": secondData}) + dangerTask := uploadIntegrationData(t, content, ctx, repository.ID, "phase3-danger.tar.gz", dangerousArchive) + dangerOutcome := executeIntegrationTask(t, tasks, handler, dangerTask.ID) + if dangerOutcome.Status != request.RepositoryTaskStatusFailed || dangerOutcome.ErrorCode != "DANGEROUS_ARCHIVE" { + t.Fatalf("dangerous archive outcome=%+v", dangerOutcome) + } + unchangedRevision, err := contentRepo.LatestReadyRevision(repository.ID, nil) + if err != nil || unchangedRevision.ID != secondRevision.ID { + t.Fatalf("dangerous archive changed revision: got=%+v want=%s err=%v", + unchangedRevision, secondRevision.ID, err) + } + + x86Data, err := os.ReadFile(x86RPM) + if err != nil { + t.Fatal(err) + } + archTask := uploadIntegrationData(t, content, ctx, armRepository.ID, filepath.Base(x86RPM), x86Data) + archOutcome := executeIntegrationTask(t, tasks, handler, archTask.ID) + if archOutcome.Status != request.RepositoryTaskStatusFailed || archOutcome.ErrorCode != "ALL_PACKAGES_REJECTED" { + t.Fatalf("architecture outcome=%+v", archOutcome) + } + archQuarantine, archPage, err := contentRepo.ListQuarantine(request.ListRepositoryQuarantineRequest{ + RepositoryID: armRepository.ID, TaskID: archTask.ID, + PageRequest: request.PageRequest{PageNumber: 1, PageSize: 20}, + }, nil) + if err != nil || archPage.Count != 1 || len(archQuarantine) != 1 || + archQuarantine[0].ReasonCode != request.RepositoryQuarantineWrongArch { + t.Fatalf("architecture quarantine=%+v page=%+v err=%v", archQuarantine, archPage, err) + } + if revision, err := contentRepo.LatestReadyRevision(armRepository.ID, nil); err != nil || revision != nil { + t.Fatalf("wrong architecture published a revision=%+v err=%v", revision, err) + } + + deleteTask, err := content.DeletePackage(ctx, request.DeleteRepositoryPackageRequest{ + RepositoryID: repository.ID, NEVRA: firstPackages[0].NEVRA, + }) + if err != nil { + t.Fatal(err) + } + deleteOutcome := executeIntegrationTask(t, tasks, handler, deleteTask.ID) + if deleteOutcome.Status != request.RepositoryTaskStatusSuccess { + t.Fatalf("delete outcome=%+v", deleteOutcome) + } + deleteRevision, err := contentRepo.LatestReadyRevision(repository.ID, nil) + if err != nil || deleteRevision == nil || deleteRevision.PackageCount != 1 { + t.Fatalf("delete revision=%+v err=%v", deleteRevision, err) + } + assertIntegrationTaskRevision(t, tasks, ctx, deleteTask.ID, deleteRevision.ID) + remaining, err := contentRepo.PackagesByRevision(deleteRevision.ID, nil) + if err != nil || len(remaining) != 1 || remaining[0].NEVRA == firstPackages[0].NEVRA { + t.Fatalf("remaining packages=%+v err=%v", remaining, err) + } + t.Logf("private ingestion final_revision=%s package_count=%d quarantine_verified=true", + deleteRevision.ID, len(remaining)) +} + +func makeIntegrationTreeWritable(root string) { + _ = filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return nil + } + if info.IsDir() { + _ = os.Chmod(path, 0755) + } else { + _ = os.Chmod(path, 0644) + } + return nil + }) +} + +func assertIntegrationTaskRevision(t *testing.T, tasks repositoryTaskService.Service, ctx entity.Context, + taskID, revisionID string, +) { + t.Helper() + task, err := tasks.Describe(ctx, taskID) + if err != nil || task.RevisionID != revisionID { + t.Fatalf("task revision=%+v want=%s err=%v", task, revisionID, err) + } +} + +type integrationRepositoryManager struct { + repositories map[string]*po.SoftwareRepository +} + +func (m *integrationRepositoryManager) Describe(_ entity.Context, id string) (*po.SoftwareRepository, error) { + repository, ok := m.repositories[id] + if !ok { + return nil, repositoryrepo.ErrRepositoryNotFound + } + return repository, nil +} + +func uploadIntegrationData(t *testing.T, content repositoryContentService.Service, ctx entity.Context, + repositoryID, name string, data []byte, +) *po.RepositoryTask { + t.Helper() + digest := sha256.Sum256(data) + upload, err := content.BeginUpload(ctx, request.BeginRepositoryUploadRequest{ + RepositoryID: repositoryID, FileName: name, TotalSize: int64(len(data)), + ExpectedSHA256: hex.EncodeToString(digest[:]), + }) + if err != nil { + t.Fatal(err) + } + for offset := 0; offset < len(data); { + end := offset + 1<<20 + if end > len(data) { + end = len(data) + } + if _, err = content.AppendChunk(ctx, request.AppendRepositoryUploadChunkRequest{ + UploadID: upload.ID, Offset: int64(offset), Data: data[offset:end], + }); err != nil { + t.Fatal(err) + } + offset = end + } + _, task, err := content.CompleteUpload(ctx, upload.ID) + if err != nil { + t.Fatal(err) + } + return task +} + +func executeIntegrationTask(t *testing.T, tasks repositoryTaskService.Service, handler *Handler, + taskID string, +) Outcome { + t.Helper() + claimed, err := tasks.ClaimNext("phase3-integration", 60*time.Second) + if err != nil { + t.Fatal(err) + } + if claimed == nil || claimed.ID != taskID { + t.Fatalf("claimed task=%+v want=%s", claimed, taskID) + } + outcome := handler.Handle(context.Background(), claimed) + if outcome.RevisionID != "" { + if _, err = tasks.AttachRevision(request.AttachRepositoryTaskRevisionRequest{ + TaskID: claimed.ID, WorkerID: "phase3-integration", FencingToken: claimed.FencingToken, + RevisionID: outcome.RevisionID, + }); err != nil { + t.Fatal(err) + } + } + if _, err = tasks.Complete(request.CompleteRepositoryTaskRequest{ + TaskID: claimed.ID, WorkerID: "phase3-integration", FencingToken: claimed.FencingToken, + Status: outcome.Status, ErrorCode: outcome.ErrorCode, ErrorMessage: outcome.ErrorMessage, + }); err != nil { + t.Fatal(err) + } + return outcome +} + +func integrationTarGZ(t *testing.T, files map[string][]byte) []byte { + t.Helper() + var buffer bytes.Buffer + gz := gzip.NewWriter(&buffer) + tw := tar.NewWriter(gz) + for name, data := range files { + if err := tw.WriteHeader(&tar.Header{Name: name, Mode: 0600, Size: int64(len(data)), Typeflag: tar.TypeReg}); err != nil { + t.Fatal(err) + } + if _, err := tw.Write(data); err != nil { + t.Fatal(err) + } + } + if err := tw.Close(); err != nil { + t.Fatal(err) + } + if err := gz.Close(); err != nil { + t.Fatal(err) + } + return buffer.Bytes() +} diff --git a/manager/backend/services/footstone/repositoryworker/ingestion_test.go b/manager/backend/services/footstone/repositoryworker/ingestion_test.go new file mode 100644 index 0000000000000000000000000000000000000000..95fa022ddac5c55d4feb3f2d6023f708dd032d67 --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/ingestion_test.go @@ -0,0 +1,88 @@ +package repositoryworker + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "crypto/sha256" + "encoding/hex" + "testing" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +func TestArchitectureAllowed(t *testing.T) { + tests := []struct { + repository string + pkg string + want bool + }{ + {"x86_64", "x86_64", true}, + {"x86_64", "noarch", true}, + {"x86_64", "aarch64", false}, + {"aarch64", "aarch64", true}, + {"noarch", "x86_64", false}, + } + for _, test := range tests { + if got := architectureAllowed(test.repository, test.pkg); got != test.want { + t.Errorf("architectureAllowed(%q,%q)=%v want %v", test.repository, test.pkg, got, test.want) + } + } +} + +func TestPackageManifestIsOrderIndependent(t *testing.T) { + one := po.RepositoryPackage{NEVRA: "b-0:1-1.x86_64", SHA256: "bb", SizeBytes: 2} + two := po.RepositoryPackage{NEVRA: "a-0:1-1.noarch", SHA256: "aa", SizeBytes: 3} + first, total := packageManifest([]po.RepositoryPackage{one, two}) + second, _ := packageManifest([]po.RepositoryPackage{two, one}) + if first != second || total != 5 { + t.Fatalf("manifest first=%s second=%s total=%d", first, second, total) + } +} + +func TestFormatNEVRA(t *testing.T) { + got := formatNEVRA(RPMInfo{Name: "ocm-test", Epoch: "", Version: "1.2", Release: "3", Architecture: "noarch"}) + if got != "ocm-test-0:1.2-3.noarch" { + t.Fatalf("NEVRA=%q", got) + } +} + +func TestPackageMatchesFilters(t *testing.T) { + tests := []struct { + name string + include, exclude []string + want bool + }{ + {name: "kernel-core", include: []string{"kernel-*"}, want: true}, + {name: "kernel-debug", include: []string{"kernel-*"}, exclude: []string{"*-debug"}, want: false}, + {name: "bash", include: []string{"kernel-*"}, want: false}, + {name: "bash", exclude: []string{"kernel-*"}, want: true}, + } + for _, test := range tests { + if got := packageMatchesFilters(test.name, test.include, test.exclude); got != test.want { + t.Errorf("packageMatchesFilters(%q)=%v want %v", test.name, got, test.want) + } + } +} + +func TestDangerousTarFixture(t *testing.T) { + var archive bytes.Buffer + gz := gzip.NewWriter(&archive) + tw := tar.NewWriter(gz) + if err := tw.WriteHeader(&tar.Header{Name: "../escape.rpm", Mode: 0600, Size: 1, Typeflag: tar.TypeReg}); err != nil { + t.Fatal(err) + } + if _, err := tw.Write([]byte("x")); err != nil { + t.Fatal(err) + } + if err := tw.Close(); err != nil { + t.Fatal(err) + } + if err := gz.Close(); err != nil { + t.Fatal(err) + } + digest := sha256.Sum256(archive.Bytes()) + if len(hex.EncodeToString(digest[:])) != 64 { + t.Fatal("invalid fixture digest") + } +} diff --git a/manager/backend/services/footstone/repositoryworker/logging_executor.go b/manager/backend/services/footstone/repositoryworker/logging_executor.go new file mode 100644 index 0000000000000000000000000000000000000000..22af94d195a40212023c0fed3d53cc277be4feb2 --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/logging_executor.go @@ -0,0 +1,101 @@ +package repositoryworker + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + repositorynodepb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repositorynode" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/tasklog" +) + +type LoggingRemoteExecutor struct { + store *storage.Store + delegate RemoteExecutor + cleanupMu sync.Mutex + lastCleanup time.Time +} + +func NewLoggingRemoteExecutor(store *storage.Store, delegate RemoteExecutor) (*LoggingRemoteExecutor, error) { + if store == nil || delegate == nil { + return nil, fmt.Errorf("logging repository executor requires storage and a delegate") + } + return &LoggingRemoteExecutor{store: store, delegate: delegate}, nil +} + +func (e *LoggingRemoteExecutor) Execute(ctx context.Context, spec *repositorynodepb.RepositoryTaskSpec) ( + RemoteOutcome, error, +) { + if spec == nil { + return RemoteOutcome{}, FailedExecution("INVALID_TASK_SPEC", "repository task spec is required") + } + e.cleanupExpired() + writer, err := tasklog.New(e.store, spec.GetTaskId()) + if err != nil { + return RemoteOutcome{}, FailedExecution("TASK_LOG_INIT_FAILED", err.Error()) + } + ctx = tasklog.WithWriter(ctx, writer) + _ = tasklog.Event(ctx, "INFO", "task-start", "repository task execution started", map[string]string{ + "task_type": spec.GetTaskType().String(), "repository_id": spec.GetRepositoryId(), + "storage_node_id": spec.GetStorageNodeId(), "revision_id": spec.GetRevisionId(), + }) + outcome, executeErr := e.delegate.Execute(ctx, spec) + // CommandContext may return "signal: killed" instead of wrapping cancellation. + // Keep the persisted log status consistent with the worker's cancel report. + if errors.Is(ctx.Err(), context.Canceled) { + executeErr = fmt.Errorf("%w: executor result: %v", context.Canceled, executeErr) + } + status, code, message := "SUCCESS", "", "" + if executeErr != nil { + status = "FAILED" + if errors.Is(executeErr, context.Canceled) { + status = "CANCELED" + } + code, message = executionFailure(executeErr) + _ = tasklog.Event(ctx, "ERROR", "task-execution", message, map[string]string{"error_code": code}) + } else { + switch outcome.CompletionStatus { + case repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_PARTIAL_SUCCESS: + status, code, message = "PARTIAL_SUCCESS", outcome.ErrorCode, outcome.ErrorMessage + _ = tasklog.Event(ctx, "WARN", "task-execution", message, map[string]string{"error_code": code}) + case repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_FAILED: + status, code, message = "FAILED", outcome.ErrorCode, outcome.ErrorMessage + _ = tasklog.Event(ctx, "ERROR", "task-execution", message, map[string]string{"error_code": code}) + case repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_CANCELED: + status, code, message = "CANCELED", outcome.ErrorCode, outcome.ErrorMessage + _ = tasklog.Event(ctx, "WARN", "task-execution", message, map[string]string{"error_code": code}) + default: + _ = tasklog.Event(ctx, "INFO", "task-execution", "repository task execution completed", map[string]string{ + "revision_id": outcome.RevisionID, "package_count": fmt.Sprintf("%d", outcome.PackageCount), + "total_bytes": fmt.Sprintf("%d", outcome.TotalBytes), + }) + } + } + if _, closeErr := writer.Close(status, code, message); closeErr != nil { + return RemoteOutcome{}, FailedExecution("TASK_LOG_FINALIZE_FAILED", closeErr.Error()) + } + return outcome, executeErr +} + +func (e *LoggingRemoteExecutor) cleanupExpired() { + e.cleanupMu.Lock() + defer e.cleanupMu.Unlock() + now := time.Now().UTC() + if !e.lastCleanup.IsZero() && now.Sub(e.lastCleanup) < time.Hour { + return + } + _, _ = tasklog.CleanupExpired(e.store, now) + e.lastCleanup = now +} + +func (e *LoggingRemoteExecutor) Activate(ctx context.Context, spec *repositorynodepb.RepositoryTaskSpec, + outcome RemoteOutcome, +) error { + return e.delegate.Activate(ctx, spec, outcome) +} + +var _ RemoteExecutor = (*LoggingRemoteExecutor)(nil) diff --git a/manager/backend/services/footstone/repositoryworker/logging_executor_test.go b/manager/backend/services/footstone/repositoryworker/logging_executor_test.go new file mode 100644 index 0000000000000000000000000000000000000000..a495ecef45eb288c199bdc6f7376848bf393f5f6 --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/logging_executor_test.go @@ -0,0 +1,151 @@ +package repositoryworker + +import ( + "context" + "errors" + "fmt" + "os" + "testing" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + repositorynodepb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repositorynode" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/tasklog" +) + +const loggingExecutorTaskID = "12121212-1212-4212-8212-121212121212" + +type loggingExecutorDelegate struct { + outcome RemoteOutcome + err error +} + +func (d loggingExecutorDelegate) Execute(ctx context.Context, _ *repositorynodepb.RepositoryTaskSpec) ( + RemoteOutcome, error, +) { + _ = tasklog.Event(ctx, "INFO", "download", "downloaded package metadata", nil) + return d.outcome, d.err +} + +func (d loggingExecutorDelegate) Activate(context.Context, *repositorynodepb.RepositoryTaskSpec, + RemoteOutcome, +) error { + return nil +} + +func TestLoggingRemoteExecutorPersistsSuccessMetadata(t *testing.T) { + store, err := storage.New(config.RepositoryConfig{Root: t.TempDir(), MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + delegate := loggingExecutorDelegate{outcome: RemoteOutcome{ + RevisionID: "34343434-3434-4434-8434-343434343434", PackageCount: 2, TotalBytes: 200, + CompletionStatus: repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_SUCCESS, + }} + executor, err := NewLoggingRemoteExecutor(store, delegate) + if err != nil { + t.Fatal(err) + } + spec := &repositorynodepb.RepositoryTaskSpec{TaskId: loggingExecutorTaskID, + RepositoryId: "56565656-5656-4656-8656-565656565656", + StorageNodeId: "78787878-7878-4878-8878-787878787878", + TaskType: repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_SYNC} + if _, err = executor.Execute(context.Background(), spec); err != nil { + t.Fatal(err) + } + metadata, err := tasklog.ReadMetadata(store, spec.TaskId) + if err != nil || metadata.CompletionStatus != "SUCCESS" || metadata.LineCount != 3 || + metadata.ErrorFingerprint != "" { + t.Fatalf("success metadata = %#v/%v", metadata, err) + } +} + +func TestLoggingRemoteExecutorPersistsFailureFingerprint(t *testing.T) { + store, err := storage.New(config.RepositoryConfig{Root: t.TempDir(), MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + executor, _ := NewLoggingRemoteExecutor(store, loggingExecutorDelegate{ + err: FailedExecution("SYNC_DOWNLOAD_FAILED", "Authorization: Bearer secret-token")}) + spec := &repositorynodepb.RepositoryTaskSpec{TaskId: loggingExecutorTaskID, + RepositoryId: "56565656-5656-4656-8656-565656565656", + StorageNodeId: "78787878-7878-4878-8878-787878787878", + TaskType: repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_SYNC} + if _, err = executor.Execute(context.Background(), spec); err == nil { + t.Fatal("failed delegate returned nil error") + } + metadata, readErr := tasklog.ReadMetadata(store, spec.TaskId) + if readErr != nil || metadata.CompletionStatus != "FAILED" || metadata.ErrorFingerprint == "" || + containsSecret(metadata.Summary) { + t.Fatalf("failure metadata = %#v/%v", metadata, readErr) + } + if !errors.As(err, new(*ExecutionError)) { + t.Fatalf("execution error type = %T", err) + } +} + +func TestLoggingRemoteExecutorReopensRecoveredTask(t *testing.T) { + store, err := storage.New(config.RepositoryConfig{Root: t.TempDir(), MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + executor, err := NewLoggingRemoteExecutor(store, loggingExecutorDelegate{outcome: RemoteOutcome{ + RevisionID: "34343434-3434-4434-8434-343434343434", PackageCount: 1, TotalBytes: 100, + CompletionStatus: repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_SUCCESS, + }}) + if err != nil { + t.Fatal(err) + } + spec := &repositorynodepb.RepositoryTaskSpec{TaskId: loggingExecutorTaskID, + RepositoryId: "56565656-5656-4656-8656-565656565656", + StorageNodeId: "78787878-7878-4878-8878-787878787878", + TaskType: repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_SYNC} + if _, err = executor.Execute(context.Background(), spec); err != nil { + t.Fatal(err) + } + if _, err = executor.Execute(context.Background(), spec); err != nil { + t.Fatalf("recovered logging executor = %v", err) + } + recovered, err := store.Resolve("task-logs/" + loggingExecutorTaskID + "/recovered") + if err != nil { + t.Fatal(err) + } + entries, err := os.ReadDir(recovered) + if err != nil || len(entries) != 2 { + t.Fatalf("recovered logging executor entries = %d/%v", len(entries), err) + } + metadata, err := tasklog.ReadMetadata(store, spec.TaskId) + if err != nil || metadata.CompletionStatus != "SUCCESS" || metadata.LineCount != 3 { + t.Fatalf("current logging metadata = %#v/%v", metadata, err) + } +} + +func containsSecret(value string) bool { + return value != tasklog.Redact(value) +} + +func TestLoggingRemoteExecutorCanceledContextKeepsCanceledMetadata(t *testing.T) { + for _, delegateErr := range []error{errors.New("signal: killed"), nil} { + t.Run(fmt.Sprint(delegateErr), func(t *testing.T) { + store, err := storage.New(config.RepositoryConfig{Root: t.TempDir(), MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + executor, err := NewLoggingRemoteExecutor(store, loggingExecutorDelegate{err: delegateErr}) + if err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithCancel(context.Background()) + cancel() + _, err = executor.Execute(ctx, &repositorynodepb.RepositoryTaskSpec{TaskId: loggingExecutorTaskID}) + if !errors.Is(err, context.Canceled) { + t.Fatalf("canceled execution returned %v", err) + } + metadata, err := tasklog.ReadMetadata(store, loggingExecutorTaskID) + if err != nil || metadata.CompletionStatus != "CANCELED" { + t.Fatalf("metadata = %#v, %v", metadata, err) + } + }) + } +} diff --git a/manager/backend/services/footstone/repositoryworker/metadata_signing.go b/manager/backend/services/footstone/repositoryworker/metadata_signing.go new file mode 100644 index 0000000000000000000000000000000000000000..3f508a92a09f9990cc0281f0e9f0963cb6eff0db --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/metadata_signing.go @@ -0,0 +1,111 @@ +package repositoryworker + +import ( + "context" + "fmt" + "net/url" + "os" + "os/exec" + "path/filepath" + "strings" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +func (h *Handler) signRepositoryMetadata(ctx context.Context, repository *po.SoftwareRepository, + revision *po.RepositoryRevision, stagingPath string, +) error { + keyRef := strings.TrimSpace(repository.MetadataSigningKeyRef) + if keyRef == "" { + return nil + } + if !metadataSigningKeyRefPattern.MatchString(keyRef) { + return fmt.Errorf("metadata signing key reference is invalid") + } + keyRoot := h.store.Config().MetadataSigningKeyRoot + publicPath := filepath.Join(keyRoot, keyRef, "public.key") + privatePath := filepath.Join(keyRoot, keyRef, "private.key") + if err := validateMetadataSigningKeyFile(publicPath, false); err != nil { + return err + } + if err := validateMetadataSigningKeyFile(privatePath, true); err != nil { + return err + } + fingerprint, _, err := metadataPublicKeyIdentity(ctx, publicPath) + if err != nil { + return err + } + if expected := strings.ToUpper(strings.TrimSpace(repository.MetadataSigningFingerprint)); expected != "" && expected != fingerprint { + return fmt.Errorf("metadata signing key fingerprint changed after task claim") + } + publicName := strings.TrimSpace(repository.MetadataSigningPublicKeyName) + if publicName == "" { + publicName = "RPM-GPG-KEY-OCMANAGER-" + keyRef + } + if filepath.Base(publicName) != publicName || strings.ContainsAny(publicName, "\\/\r\n\t") { + return fmt.Errorf("metadata signing public key name is invalid") + } + home, err := os.MkdirTemp("", "ocmanager-repository-sign-") + if err != nil { + return err + } + defer os.RemoveAll(home) + if err = os.Chmod(home, 0700); err != nil { + return err + } + if err = runGPG(ctx, home, "--import", privatePath); err != nil { + return fmt.Errorf("import node-local metadata signing key: %w", err) + } + repomdPath := filepath.Join(stagingPath, "repodata", "repomd.xml") + signaturePath := repomdPath + ".asc" + if err = runGPG(ctx, home, "--yes", "--armor", "--local-user", fingerprint, + "--detach-sign", "--output", signaturePath, repomdPath); err != nil { + return fmt.Errorf("sign repomd.xml: %w", err) + } + if err = runGPG(ctx, home, "--verify", signaturePath, repomdPath); err != nil { + return fmt.Errorf("verify repomd.xml signature: %w", err) + } + keysDirectory := filepath.Join(stagingPath, "keys") + if err = os.MkdirAll(keysDirectory, 0755); err != nil { + return err + } + if err = copyFile(publicPath, filepath.Join(keysDirectory, publicName)); err != nil { + return fmt.Errorf("publish metadata signing public key: %w", err) + } + publicURL, err := metadataPublicKeyURL(repository, revision.ID, publicName, h.store.Config().PublicBaseURL) + if err != nil { + return err + } + revision.MetadataSigned = true + revision.MetadataSigningKeyRef = keyRef + revision.MetadataSigningFingerprint = fingerprint + revision.RepomdSignaturePath = "repodata/repomd.xml.asc" + revision.MetadataPublicKeyURL = publicURL + return nil +} + +func runGPG(ctx context.Context, home string, args ...string) error { + base := []string{"--batch", "--no-tty", "--homedir", home} + command := exec.CommandContext(ctx, "gpg", append(base, args...)...) + command.Env = commandEnvironment() + if output, err := command.CombinedOutput(); err != nil { + return fmt.Errorf("%s", truncateOutput(output)) + } + return nil +} + +func metadataPublicKeyURL(repository *po.SoftwareRepository, revisionID, publicName, fallbackBase string) (string, error) { + base := strings.TrimRight(strings.TrimSpace(repository.PublishBaseURLSnapshot), "/") + if base == "" { + base = strings.TrimRight(strings.TrimSpace(fallbackBase), "/") + } + value, err := url.Parse(base) + if err != nil || (value.Scheme != "http" && value.Scheme != "https") || value.Host == "" || + value.User != nil || value.RawQuery != "" || value.Fragment != "" { + return "", fmt.Errorf("metadata signing requires a credential-free HTTP(S) publish base URL") + } + value.Path = strings.TrimRight(value.Path, "/") + "/" + url.PathEscape(repository.PublishKey) + + "/revisions/" + url.PathEscape(revisionID) + "/keys/" + url.PathEscape(publicName) + value.RawPath = "" + return value.String(), nil +} diff --git a/manager/backend/services/footstone/repositoryworker/metadata_signing_keys.go b/manager/backend/services/footstone/repositoryworker/metadata_signing_keys.go new file mode 100644 index 0000000000000000000000000000000000000000..602d130aa548aeb4eb6bd2cb0a0d7f6ed795a35c --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/metadata_signing_keys.go @@ -0,0 +1,130 @@ +package repositoryworker + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" + + repositorynodepb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repositorynode" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/repositoryworker/controlplane" +) + +const maximumMetadataSigningKeyBytes = int64(1 << 20) + +var metadataSigningKeyRefPattern = regexp.MustCompile(`^[A-Za-z0-9._+-]{1,128}$`) + +func applyMetadataSigningReference(repository *po.SoftwareRepository, + value *repositorynodepb.RepositoryMetadataSigningReference, +) { + if repository == nil || value == nil { + return + } + repository.MetadataSigningKeyRef = value.GetKeyRef() + repository.MetadataSigningFingerprint = value.GetFingerprint() + repository.MetadataSigningPublicKeyName = value.GetPublicKeyName() +} + +// scanMetadataSigningKeys reads public identity only. Private key bytes and +// private paths never leave the repository node. +func scanMetadataSigningKeys(ctx context.Context, root string, observedAt time.Time) ( + []controlplane.MetadataSigningKey, error, +) { + entries, err := os.ReadDir(root) + if os.IsNotExist(err) { + return []controlplane.MetadataSigningKey{}, nil + } + if err != nil { + return nil, fmt.Errorf("read metadata signing key root: %w", err) + } + values := make([]controlplane.MetadataSigningKey, 0, len(entries)) + for _, entry := range entries { + keyRef := entry.Name() + if !entry.IsDir() || !metadataSigningKeyRefPattern.MatchString(keyRef) { + return nil, fmt.Errorf("metadata signing key entry %q is invalid", keyRef) + } + directory := filepath.Join(root, keyRef) + publicPath, privatePath := filepath.Join(directory, "public.key"), filepath.Join(directory, "private.key") + if err = validateMetadataSigningKeyFile(publicPath, false); err != nil { + return nil, fmt.Errorf("metadata signing key %s public key: %w", keyRef, err) + } + if err = validateMetadataSigningKeyFile(privatePath, true); err != nil { + return nil, fmt.Errorf("metadata signing key %s private key: %w", keyRef, err) + } + fingerprint, createdAt, identityErr := metadataPublicKeyIdentity(ctx, publicPath) + if identityErr != nil { + return nil, fmt.Errorf("metadata signing key %s: %w", keyRef, identityErr) + } + values = append(values, controlplane.MetadataSigningKey{KeyRef: keyRef, Fingerprint: fingerprint, + PublicKeyName: "RPM-GPG-KEY-OCMANAGER-" + keyRef, Status: "AVAILABLE", + KeyCreatedAt: createdAt, ObservedAt: observedAt}) + } + return values, nil +} + +func validateMetadataSigningKeyFile(path string, private bool) error { + info, err := os.Lstat(path) + if err != nil { + return err + } + if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 || info.Size() <= 0 || + info.Size() > maximumMetadataSigningKeyBytes { + return fmt.Errorf("file type or size is invalid") + } + if private && info.Mode().Perm()&0077 != 0 { + return fmt.Errorf("private key permissions must not grant group or other access") + } + return nil +} + +func metadataPublicKeyIdentity(ctx context.Context, path string) (string, *time.Time, error) { + home, err := os.MkdirTemp("", "ocmanager-repository-key-inspect-") + if err != nil { + return "", nil, fmt.Errorf("create temporary GPG home: %w", err) + } + defer os.RemoveAll(home) + if err = os.Chmod(home, 0700); err != nil { + return "", nil, fmt.Errorf("secure temporary GPG home: %w", err) + } + command := exec.CommandContext(ctx, "gpg", "--batch", "--no-tty", "--homedir", home, + "--show-keys", "--with-colons", path) + command.Env = commandEnvironment() + output, err := command.CombinedOutput() + if err != nil { + return "", nil, fmt.Errorf("inspect public key: %s", truncateOutput(output)) + } + var fingerprint string + var createdAt *time.Time + seenPublic := false + for _, line := range strings.Split(string(output), "\n") { + fields := strings.Split(line, ":") + if len(fields) < 10 { + continue + } + switch fields[0] { + case "pub": + if seenPublic { + return "", nil, fmt.Errorf("public key file contains multiple primary keys") + } + seenPublic = true + if seconds, parseErr := strconv.ParseInt(fields[5], 10, 64); parseErr == nil && seconds > 0 { + value := time.Unix(seconds, 0).UTC() + createdAt = &value + } + case "fpr": + if seenPublic && fingerprint == "" { + fingerprint = strings.ToUpper(strings.TrimSpace(fields[9])) + } + } + } + if !seenPublic || len(fingerprint) < 40 || len(fingerprint) > 64 { + return "", nil, fmt.Errorf("public key fingerprint is unavailable") + } + return fingerprint, createdAt, nil +} diff --git a/manager/backend/services/footstone/repositoryworker/metadata_signing_test.go b/manager/backend/services/footstone/repositoryworker/metadata_signing_test.go new file mode 100644 index 0000000000000000000000000000000000000000..8b377bd6e9f8df891649c43656e4507c3c35e53d --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/metadata_signing_test.go @@ -0,0 +1,230 @@ +package repositoryworker + +import ( + "context" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + appconfig "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" +) + +func TestMetadataPublicKeyURLUsesImmutableRevision(t *testing.T) { + repository := &po.SoftwareRepository{PublishKey: "repo-key", + PublishBaseURLSnapshot: "https://repo.example.test/repositories"} + value, err := metadataPublicKeyURL(repository, "11111111-1111-4111-8111-111111111111", + "RPM-GPG-KEY-OCMANAGER-release", "") + if err != nil { + t.Fatal(err) + } + want := "https://repo.example.test/repositories/repo-key/revisions/11111111-1111-4111-8111-111111111111/keys/RPM-GPG-KEY-OCMANAGER-release" + if value != want { + t.Fatalf("metadata public key URL=%q, want %q", value, want) + } +} + +func TestMetadataPublicKeyIdentityDoesNotRequireWritableHome(t *testing.T) { + if runtime.GOOS != "linux" { + t.Skip("read-only home regression test is Linux-specific") + } + if _, err := exec.LookPath("gpg"); err != nil { + t.Skip("gpg is unavailable") + } + home := metadataTestGPGHome(t) + identity := "OCManager Read-only Home Test " + if output, err := exec.Command("gpg", "--batch", "--homedir", home, + "--passphrase", "", "--quick-generate-key", identity, "rsa2048", "sign", "1d").CombinedOutput(); err != nil { + t.Fatalf("generate metadata signing key: %v: %s", err, output) + } + publicPath := filepath.Join(t.TempDir(), "public.key") + exportGPGFileWithHome(t, home, publicPath, "--armor", "--export", identity) + t.Setenv("HOME", "/proc/ocmanager-read-only-gpg-home") + fingerprint, _, err := metadataPublicKeyIdentity(context.Background(), publicPath) + if err != nil || len(fingerprint) < 40 { + t.Fatalf("inspect metadata key with read-only HOME: fingerprint=%q err=%v", fingerprint, err) + } +} + +func TestSignRepositoryMetadataCreatesVerifiableImmutableArtifacts(t *testing.T) { + if _, err := exec.LookPath("gpg"); err != nil { + t.Skip("gpg is unavailable") + } + keyRoot := filepath.Join(t.TempDir(), "metadata-keys") + keyRef := "release-2026" + keyDirectory := filepath.Join(keyRoot, keyRef) + if err := os.MkdirAll(keyDirectory, 0700); err != nil { + t.Fatal(err) + } + generateHome := metadataTestGPGHome(t) + identity := "OCManager Metadata Test " + if output, err := exec.Command("gpg", "--batch", "--homedir", generateHome, + "--passphrase", "", "--quick-generate-key", identity, "rsa2048", "sign", "1d").CombinedOutput(); err != nil { + t.Fatalf("generate metadata signing key: %v: %s", err, output) + } + publicPath := filepath.Join(keyDirectory, "public.key") + privatePath := filepath.Join(keyDirectory, "private.key") + exportGPGFileWithHome(t, generateHome, publicPath, "--armor", "--export", identity) + exportGPGFileWithHome(t, generateHome, privatePath, "--armor", "--export-secret-keys", identity) + if err := os.Chmod(privatePath, 0600); err != nil { + t.Fatal(err) + } + root := t.TempDir() + store, err := storage.New(appconfig.RepositoryConfig{Root: root, + PublicBaseURL: "https://repo.example.test/repositories", MetadataSigningKeyRoot: keyRoot}) + if err != nil { + t.Fatal(err) + } + staging := filepath.Join(root, "staging", "publish", "metadata-test") + if err = os.MkdirAll(filepath.Join(staging, "repodata"), 0750); err != nil { + t.Fatal(err) + } + if err = os.WriteFile(filepath.Join(staging, "repodata", "repomd.xml"), []byte("\n"), 0644); err != nil { + t.Fatal(err) + } + repository := &po.SoftwareRepository{PublishKey: "signed-repository", + PublishBaseURLSnapshot: "https://repo.example.test/repositories", MetadataSigningKeyRef: keyRef} + revision := &po.RepositoryRevision{ID: "11111111-1111-4111-8111-111111111111"} + handler := &Handler{store: store} + if err = handler.signRepositoryMetadata(context.Background(), repository, revision, staging); err != nil { + t.Fatal(err) + } + if !revision.MetadataSigned || revision.MetadataSigningFingerprint == "" || + revision.RepomdSignaturePath != "repodata/repomd.xml.asc" || + !strings.Contains(revision.MetadataPublicKeyURL, "/revisions/"+revision.ID+"/keys/") { + t.Fatalf("metadata signing result=%+v", revision) + } + verifyHome := metadataTestGPGHome(t) + publishedKey := filepath.Join(staging, "keys", "RPM-GPG-KEY-OCMANAGER-"+keyRef) + if output, err := exec.Command("gpg", "--batch", "--homedir", verifyHome, + "--import", publishedKey).CombinedOutput(); err != nil { + t.Fatalf("import published key: %v: %s", err, output) + } + if output, err := exec.Command("gpg", "--batch", "--homedir", verifyHome, "--verify", + filepath.Join(staging, revision.RepomdSignaturePath), + filepath.Join(staging, "repodata", "repomd.xml")).CombinedOutput(); err != nil { + t.Fatalf("verify published repomd signature: %v: %s", err, output) + } +} + +func TestSignedRepositoryMetadataPassesIsolatedDNFVerification(t *testing.T) { + for _, tool := range []string{"gpg", "createrepo_c", "dnf"} { + if _, err := exec.LookPath(tool); err != nil { + t.Skipf("%s is unavailable", tool) + } + } + keyRoot := filepath.Join(t.TempDir(), "metadata-keys") + keyRef := "dnf-release" + keyDirectory := filepath.Join(keyRoot, keyRef) + if err := os.MkdirAll(keyDirectory, 0700); err != nil { + t.Fatal(err) + } + generateHome := metadataTestGPGHome(t) + identity := "OCManager DNF Metadata Test " + if output, err := exec.Command("gpg", "--batch", "--homedir", generateHome, + "--passphrase", "", "--quick-generate-key", identity, "rsa2048", "sign", "1d").CombinedOutput(); err != nil { + t.Fatalf("generate metadata signing key: %v: %s", err, output) + } + publicPath := filepath.Join(keyDirectory, "public.key") + privatePath := filepath.Join(keyDirectory, "private.key") + exportGPGFileWithHome(t, generateHome, publicPath, "--armor", "--export", identity) + exportGPGFileWithHome(t, generateHome, privatePath, "--armor", "--export-secret-keys", identity) + if err := os.Chmod(privatePath, 0600); err != nil { + t.Fatal(err) + } + + root := t.TempDir() + publicRoot := filepath.Join(root, "public") + server := httptest.NewServer(http.FileServer(http.Dir(publicRoot))) + defer server.Close() + store, err := storage.New(appconfig.RepositoryConfig{Root: root, + PublicBaseURL: server.URL, MetadataSigningKeyRoot: keyRoot}) + if err != nil { + t.Fatal(err) + } + revision := &po.RepositoryRevision{ID: "22222222-2222-4222-8222-222222222222"} + repository := &po.SoftwareRepository{PublishKey: "dnf-metadata-signed", + PublishBaseURLSnapshot: server.URL, MetadataSigningKeyRef: keyRef} + revisionRoot := filepath.Join(publicRoot, repository.PublishKey, "revisions", revision.ID) + if err = os.MkdirAll(revisionRoot, 0755); err != nil { + t.Fatal(err) + } + if output, createErr := exec.Command("createrepo_c", revisionRoot).CombinedOutput(); createErr != nil { + t.Fatalf("create repository metadata: %v: %s", createErr, output) + } + handler := &Handler{store: store} + if err = handler.signRepositoryMetadata(context.Background(), repository, revision, revisionRoot); err != nil { + t.Fatal(err) + } + + dnfRoot := t.TempDir() + repoID := "ocmanager-metadata-signed" + config := "[main]\nreposdir=" + filepath.Join(dnfRoot, "repos.d") + + "\ncachedir=" + filepath.Join(dnfRoot, "cache") + + "\npersistdir=" + filepath.Join(dnfRoot, "persist") + "\n\n[" + repoID + "]\n" + + "name=OCManager metadata signing integration\nbaseurl=" + server.URL + "/" + repository.PublishKey + + "/revisions/" + revision.ID + "\nenabled=1\ngpgcheck=0\nrepo_gpgcheck=1\ngpgkey=" + + revision.MetadataPublicKeyURL + "\n" + if err = os.MkdirAll(filepath.Join(dnfRoot, "repos.d"), 0700); err != nil { + t.Fatal(err) + } + configPath := filepath.Join(dnfRoot, "dnf.conf") + if err = os.WriteFile(configPath, []byte(config), 0600); err != nil { + t.Fatal(err) + } + command := exec.Command("dnf", "-y", "-c", configPath, "--disablerepo=*", "--enablerepo="+repoID, + "--setopt=metadata_expire=0", "makecache") + command.Env = commandEnvironment() + if output, commandErr := command.CombinedOutput(); commandErr != nil { + t.Fatalf("isolated DNF metadata verification: %v: %s", commandErr, output) + } + t.Logf("repo_id=%s repo_gpgcheck=1 metadata_signed=%t dnf_exit_code=0", repoID, revision.MetadataSigned) +} + +// Go's t.TempDir includes the full test name, which can exceed the Unix socket +// pathname limit used by gpg-agent even when TMPDIR itself is reasonably short. +func metadataTestGPGHome(t *testing.T) string { + t.Helper() + home, err := os.MkdirTemp("", "ocm-gpg-") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + // Stop only the agent belonging to this test's isolated keyring. + _ = exec.Command("gpgconf", "--homedir", home, "--kill", "gpg-agent").Run() + if err := os.RemoveAll(home); err != nil { + t.Errorf("remove isolated GPG home: %v", err) + } + }) + return home +} + +func exportGPGFileWithHome(t *testing.T, home, destination string, args ...string) { + t.Helper() + command := exec.Command("gpg", append([]string{"--batch", "--homedir", home}, args...)...) + output, err := command.Output() + if err != nil { + t.Fatal(err) + } + if err = os.WriteFile(destination, output, 0600); err != nil { + t.Fatal(err) + } +} + +func TestSignatureAuditDoesNotRejectAndEnforceDoes(t *testing.T) { + audit := &signatureSession{enforce: false} + result, err := audit.evaluationFailure("UNSIGNED", "", "UNSIGNED_RPM", "RPM is not signed") + if err != nil || result.Status != "UNSIGNED" { + t.Fatalf("audit result=%+v err=%v", result, err) + } + enforce := &signatureSession{enforce: true} + if _, err = enforce.evaluationFailure("UNSIGNED", "", "UNSIGNED_RPM", "RPM is not signed"); err == nil { + t.Fatal("enforce policy accepted an unsigned RPM") + } +} diff --git a/manager/backend/services/footstone/repositoryworker/node_probe.go b/manager/backend/services/footstone/repositoryworker/node_probe.go new file mode 100644 index 0000000000000000000000000000000000000000..5be53bde124702f66aa7f4aa4f9101782660d4a1 --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/node_probe.go @@ -0,0 +1,149 @@ +package repositoryworker + +import ( + "bufio" + "context" + "fmt" + "net" + "os" + "os/exec" + "runtime" + "runtime/debug" + "strings" + "sync" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/repositoryworker/controlplane" +) + +type RepositoryNodeProbe struct { + store *storage.Store + tools []string + usageMu sync.Mutex + usageScanInterval time.Duration + usageScannedAt time.Time + repositoryUsages []controlplane.RepositoryUsage +} + +func NewRepositoryNodeProbe(store *storage.Store) (*RepositoryNodeProbe, error) { + if store == nil { + return nil, fmt.Errorf("repository node probe requires storage") + } + return &RepositoryNodeProbe{store: store, + tools: []string{"rpm", "rpmkeys", "dnf", "createrepo_c", "gpg"}, usageScanInterval: time.Minute}, nil +} + +func (p *RepositoryNodeProbe) Probe(ctx context.Context) (controlplane.NodeHeartbeat, error) { + capacity, err := p.store.Capacity() + if err != nil { + return controlplane.NodeHeartbeat{}, fmt.Errorf("probe repository filesystem: %w", err) + } + value := controlplane.NodeHeartbeat{FilesystemType: capacity.FilesystemType, + CapacityBytes: capacity.CapacityBytes, FreeBytes: capacity.FreeBytes, FreeInodes: capacity.FreeInodes, + ToolVersions: make(map[string]string, len(p.tools))} + value.Hostname, _ = os.Hostname() + value.IPAddress = primaryIPAddress() + value.Architecture = nodeArchitecture() + value.OSName, value.OSVersion = operatingSystemIdentity() + value.AgentVersion = repositoryNodeVersion() + if usages, usageErr := p.collectRepositoryUsages(ctx, time.Now().UTC()); usageErr != nil { + value.ErrorCode = "REPOSITORY_NODE_USAGE_SCAN_FAILED" + value.ErrorMessage = usageErr.Error() + } else { + value.RepositoryUsages = usages + value.RepositoryUsageSnapshot = true + } + if keys, keyErr := scanMetadataSigningKeys(ctx, p.store.Config().MetadataSigningKeyRoot, time.Now().UTC()); keyErr != nil { + value.ErrorCode = "REPOSITORY_METADATA_SIGNING_KEY_INVALID" + value.ErrorMessage = keyErr.Error() + } else { + value.MetadataSigningKeys = keys + value.MetadataKeySnapshot = true + } + missing := make([]string, 0) + for _, tool := range p.tools { + path, lookupErr := exec.LookPath(tool) + if lookupErr != nil { + missing = append(missing, tool) + continue + } + command := exec.CommandContext(ctx, path, "--version") + output, commandErr := command.CombinedOutput() + if commandErr != nil { + missing = append(missing, tool) + continue + } + version := strings.TrimSpace(string(output)) + if index := strings.IndexByte(version, '\n'); index >= 0 { + version = version[:index] + } + if len(version) > 128 { + version = version[:128] + } + value.ToolVersions[tool] = version + } + if len(missing) > 0 { + value.ErrorCode = "REPOSITORY_NODE_TOOL_MISSING" + value.ErrorMessage = "required repository tools are missing or unusable: " + strings.Join(missing, ", ") + } + return value, nil +} + +func nodeArchitecture() string { + switch runtime.GOARCH { + case "amd64": + return "x86_64" + case "arm64": + return "aarch64" + default: + return runtime.GOARCH + } +} + +func primaryIPAddress() string { + addresses, err := net.InterfaceAddrs() + if err != nil { + return "" + } + for _, address := range addresses { + ip, _, parseErr := net.ParseCIDR(address.String()) + if parseErr == nil && ip != nil && !ip.IsLoopback() && ip.To4() != nil { + return ip.String() + } + } + return "" +} + +func operatingSystemIdentity() (string, string) { + file, err := os.Open("/etc/os-release") + if err != nil { + return runtime.GOOS, "" + } + defer file.Close() + values := map[string]string{} + scanner := bufio.NewScanner(file) + for scanner.Scan() { + key, value, found := strings.Cut(scanner.Text(), "=") + if found { + values[key] = strings.Trim(strings.TrimSpace(value), `"`) + } + } + name := values["NAME"] + if name == "" { + name = runtime.GOOS + } + return name, values["VERSION_ID"] +} + +func repositoryNodeVersion() string { + if value := strings.TrimSpace(os.Getenv("OCM_REPOSITORY_NODE_VERSION")); value != "" { + return value + } + if info, ok := debug.ReadBuildInfo(); ok && strings.TrimSpace(info.Main.Version) != "" { + return info.Main.Version + } + return "development" +} + +var _ NodeProbe = (*RepositoryNodeProbe)(nil) diff --git a/manager/backend/services/footstone/repositoryworker/node_probe_test.go b/manager/backend/services/footstone/repositoryworker/node_probe_test.go new file mode 100644 index 0000000000000000000000000000000000000000..492a548e13da94754b1b377b7f0c692e5af64bbe --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/node_probe_test.go @@ -0,0 +1,54 @@ +package repositoryworker + +import ( + "context" + "os" + "path/filepath" + "testing" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" +) + +func TestRepositoryNodeProbeReportsCapacityAndMissingTools(t *testing.T) { + store, err := storage.New(config.RepositoryConfig{Root: t.TempDir(), MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + probe, err := NewRepositoryNodeProbe(store) + if err != nil { + t.Fatal(err) + } + probe.tools = []string{"definitely-not-an-ocmanager-tool"} + value, err := probe.Probe(context.Background()) + if err != nil { + t.Fatal(err) + } + if value.CapacityBytes <= 0 || value.FreeBytes < 0 || value.FreeInodes < 0 || value.FilesystemType == "" { + t.Fatalf("capacity heartbeat = %#v", value) + } + if value.ErrorCode != "REPOSITORY_NODE_TOOL_MISSING" { + t.Fatalf("missing tool error = %#v", value) + } +} + +func TestRepositoryNodeProbeCapturesBoundedVersion(t *testing.T) { + root := t.TempDir() + tool := filepath.Join(root, "repo-tool") + if err := os.WriteFile(tool, []byte("#!/bin/sh\nprintf 'repo-tool 1.2.3\\nextra\\n'\n"), 0755); err != nil { + t.Fatal(err) + } + store, err := storage.New(config.RepositoryConfig{Root: filepath.Join(root, "data"), MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + probe, _ := NewRepositoryNodeProbe(store) + probe.tools = []string{tool} + value, err := probe.Probe(context.Background()) + if err != nil { + t.Fatal(err) + } + if value.ErrorCode != "" || value.ToolVersions[tool] != "repo-tool 1.2.3" { + t.Fatalf("tool heartbeat = %#v", value) + } +} diff --git a/manager/backend/services/footstone/repositoryworker/nodeapi/server.go b/manager/backend/services/footstone/repositoryworker/nodeapi/server.go new file mode 100644 index 0000000000000000000000000000000000000000..2ecf3318d7edf8bd044897e02a87c8d949dd05aa --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/nodeapi/server.go @@ -0,0 +1,562 @@ +package nodeapi + +import ( + "bytes" + "context" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "net/http" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" + + "github.com/google/uuid" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/nodeprotocol" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/tasklog" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/repositoryworker/controlplane" +) + +type nodeProbe interface { + Probe(context.Context) (controlplane.NodeHeartbeat, error) +} + +type Server struct { + cfg config.RepositoryNodeServerConfig + store *storage.Store + probe nodeProbe + handler http.Handler +} + +func New(cfg config.RepositoryNodeServerConfig, store *storage.Store, probes ...nodeProbe) (*Server, error) { + if store == nil { + return nil, errors.New("repository node server requires repository storage") + } + if cfg.MaxResultBytes <= 0 { + cfg.MaxResultBytes = 64 << 20 + } + if cfg.ReadTimeoutSeconds <= 0 { + cfg.ReadTimeoutSeconds = 30 + } + server := &Server{cfg: cfg, store: store} + if len(probes) > 0 { + server.probe = probes[0] + } + mux := http.NewServeMux() + mux.HandleFunc(nodeprotocol.ProbePath, server.serveProbe) + mux.HandleFunc(nodeprotocol.TaskResultPathPrefix, server.serveTaskResult) + mux.HandleFunc(nodeprotocol.UploadPathPrefix, server.serveUpload) + mux.HandleFunc(nodeprotocol.GPGKeyPathPrefix, server.serveGPGKey) + mux.HandleFunc(nodeprotocol.TaskLogPathPrefix, server.serveTaskLog) + server.handler = mux + return server, nil +} + +func (s *Server) serveProbe(response http.ResponseWriter, httpRequest *http.Request) { + if !verifiedClient(httpRequest) { + http.Error(response, "verified mTLS client certificate required", http.StatusUnauthorized) + return + } + if httpRequest.URL.Path != nodeprotocol.ProbePath { + http.NotFound(response, httpRequest) + return + } + if httpRequest.Method != http.MethodGet && httpRequest.Method != http.MethodHead { + response.Header().Set("Allow", "GET, HEAD") + http.Error(response, "method not allowed", http.StatusMethodNotAllowed) + return + } + if s.probe == nil { + http.Error(response, "repository node probe is unavailable", http.StatusServiceUnavailable) + return + } + value, err := s.probe.Probe(httpRequest.Context()) + if err != nil { + http.Error(response, "repository node probe failed", http.StatusServiceUnavailable) + return + } + usages := make([]nodeprotocol.RepositoryUsage, 0, len(value.RepositoryUsages)) + for _, usage := range value.RepositoryUsages { + usages = append(usages, nodeprotocol.RepositoryUsage{RepositoryID: usage.RepositoryID, + LogicalBytes: usage.LogicalBytes, PhysicalBytes: usage.PhysicalBytes, + PublishStatus: usage.PublishStatus, ObservedAt: usage.ObservedAt}) + } + result := nodeprotocol.ProbeResponse{Hostname: value.Hostname, IPAddress: value.IPAddress, + Architecture: value.Architecture, OSName: value.OSName, OSVersion: value.OSVersion, + AgentVersion: value.AgentVersion, FilesystemType: value.FilesystemType, + CapacityBytes: value.CapacityBytes, FreeBytes: value.FreeBytes, FreeInodes: value.FreeInodes, + ToolVersions: value.ToolVersions, ErrorCode: value.ErrorCode, ErrorMessage: value.ErrorMessage, + ObservedAt: time.Now().UTC(), RepositoryUsages: usages, + RepositoryUsageSnapshot: value.RepositoryUsageSnapshot} + if httpRequest.Method == http.MethodHead { + response.Header().Set("Cache-Control", "no-store") + response.WriteHeader(http.StatusOK) + return + } + writeJSON(response, http.StatusOK, result) +} + +func (s *Server) serveTaskLog(response http.ResponseWriter, httpRequest *http.Request) { + if !verifiedClient(httpRequest) { + http.Error(response, "verified mTLS client certificate required", http.StatusUnauthorized) + return + } + if httpRequest.Method != http.MethodGet && httpRequest.Method != http.MethodHead { + response.Header().Set("Allow", "GET, HEAD") + http.Error(response, "method not allowed", http.StatusMethodNotAllowed) + return + } + suffix := strings.TrimPrefix(httpRequest.URL.Path, nodeprotocol.TaskLogPathPrefix) + parts := strings.Split(suffix, "/") + if suffix == httpRequest.URL.Path || len(parts) != 2 || uuid.Validate(parts[0]) != nil || + (parts[1] != "metadata" && parts[1] != "content") { + http.NotFound(response, httpRequest) + return + } + metadata, err := tasklog.ReadMetadata(s.store, parts[0]) + if err != nil { + http.NotFound(response, httpRequest) + return + } + response.Header().Set("Cache-Control", "no-store") + response.Header().Set("X-Content-Type-Options", "nosniff") + if parts[1] == "metadata" { + if httpRequest.Method == http.MethodHead { + response.WriteHeader(http.StatusOK) + return + } + writeJSON(response, http.StatusOK, metadata) + return + } + path, err := s.store.Resolve(metadata.RelativePath) + if err != nil { + http.Error(response, "task log path is invalid", http.StatusUnprocessableEntity) + return + } + info, err := os.Lstat(path) + if err != nil || !info.Mode().IsRegular() || info.Size() != metadata.CompressedBytes { + http.Error(response, "task log is unavailable", http.StatusUnprocessableEntity) + return + } + file, err := os.Open(filepath.Clean(path)) + if err != nil { + http.Error(response, "task log is unavailable", http.StatusInternalServerError) + return + } + defer file.Close() + response.Header().Set(nodeprotocol.HeaderObjectID, metadata.RelativePath) + response.Header().Set(nodeprotocol.HeaderSHA256, metadata.SHA256) + response.Header().Set("Content-Type", "application/zstd") + response.Header().Set("Content-Length", strconv.FormatInt(info.Size(), 10)) + response.WriteHeader(http.StatusOK) + if httpRequest.Method == http.MethodGet { + _, _ = io.Copy(response, file) + } +} + +func (s *Server) serveUpload(response http.ResponseWriter, httpRequest *http.Request) { + if !verifiedClient(httpRequest) { + http.Error(response, "verified mTLS client certificate required", http.StatusUnauthorized) + return + } + suffix := strings.TrimPrefix(httpRequest.URL.Path, nodeprotocol.UploadPathPrefix) + parts := strings.Split(suffix, "/") + if suffix == httpRequest.URL.Path || len(parts) == 0 || len(parts) > 3 || uuid.Validate(parts[0]) != nil { + http.NotFound(response, httpRequest) + return + } + uploadID := parts[0] + action := "" + if len(parts) >= 2 { + action = parts[1] + } + switch { + case httpRequest.Method == http.MethodPost && action == "begin": + s.beginUpload(response, httpRequest, uploadID) + case httpRequest.Method == http.MethodPut && action == "content": + s.appendUpload(response, httpRequest, uploadID) + case httpRequest.Method == http.MethodPut && action == "chunks" && len(parts) == 3: + s.putUploadChunk(response, httpRequest, uploadID, parts[2]) + case httpRequest.Method == http.MethodPost && action == "complete": + s.completeUpload(response, httpRequest, uploadID) + case httpRequest.Method == http.MethodDelete && action == "": + s.abortUpload(response, uploadID) + default: + response.Header().Set("Allow", "POST, PUT, DELETE") + http.Error(response, "method or upload action not allowed", http.StatusMethodNotAllowed) + } +} + +func (s *Server) putUploadChunk(response http.ResponseWriter, httpRequest *http.Request, uploadID, rawIndex string) { + chunkIndex, err := strconv.Atoi(rawIndex) + if err != nil || chunkIndex < 0 { + http.Error(response, "chunk index is invalid", http.StatusBadRequest) + return + } + length, err := strconv.ParseInt(httpRequest.Header.Get(nodeprotocol.HeaderChunkLength), 10, 64) + if err != nil || length <= 0 || length > int64(s.store.Config().MaxChunkBytes) { + http.Error(response, "chunk length is invalid", http.StatusBadRequest) + return + } + expectedSHA := strings.ToLower(httpRequest.Header.Get(nodeprotocol.HeaderChunkSHA256)) + if !digestPattern.MatchString(expectedSHA) { + http.Error(response, "chunk digest is invalid", http.StatusBadRequest) + return + } + content, err := io.ReadAll(io.LimitReader(httpRequest.Body, length+1)) + if err != nil || int64(len(content)) != length { + http.Error(response, "chunk body length mismatch", http.StatusBadRequest) + return + } + relative, err := s.store.PutUploadChunk(uploadID, chunkIndex, content, expectedSHA) + if err != nil { + status := http.StatusConflict + if strings.Contains(err.Error(), "checksum mismatch") { + status = http.StatusUnprocessableEntity + } + http.Error(response, "upload chunk cannot be stored", status) + return + } + writeJSON(response, http.StatusOK, nodeprotocol.PutUploadChunkResponse{RelativePath: relative}) +} + +func (s *Server) serveGPGKey(response http.ResponseWriter, httpRequest *http.Request) { + if !verifiedClient(httpRequest) { + http.Error(response, "verified mTLS client certificate required", http.StatusUnauthorized) + return + } + suffix := strings.TrimPrefix(httpRequest.URL.Path, nodeprotocol.GPGKeyPathPrefix) + parts := strings.Split(suffix, "/") + if suffix == httpRequest.URL.Path || len(parts) != 2 || uuid.Validate(parts[0]) != nil { + http.NotFound(response, httpRequest) + return + } + repositoryID, fingerprint := parts[0], parts[1] + if _, err := nodeprotocol.GPGKeyPath(repositoryID, fingerprint); err != nil { + http.NotFound(response, httpRequest) + return + } + publishKey := strings.TrimSpace(httpRequest.Header.Get(nodeprotocol.HeaderPublishKey)) + privatePath, pathErr := s.store.GPGKeyRelativePath(repositoryID, fingerprint) + publicPath, publicPathErr := s.store.PublicGPGKeyRelativePath(publishKey, fingerprint) + if pathErr != nil || publicPathErr != nil { + http.Error(response, "repository GPG key identity is invalid", http.StatusBadRequest) + return + } + switch httpRequest.Method { + case http.MethodPut: + s.putGPGKey(response, httpRequest, repositoryID, publishKey, fingerprint, privatePath, publicPath) + case http.MethodHead: + if err := s.store.VerifyGPGPublicKey(repositoryID, publishKey, fingerprint, privatePath); err != nil { + http.Error(response, "repository GPG key is not available", http.StatusNotFound) + return + } + digest, _, err := s.store.SHA256(privatePath) + if err != nil { + http.Error(response, "repository GPG key cannot be verified", http.StatusUnprocessableEntity) + return + } + response.Header().Set(nodeprotocol.HeaderPrivatePath, privatePath) + response.Header().Set(nodeprotocol.HeaderPublicPath, publicPath) + response.Header().Set(nodeprotocol.HeaderGPGKeySHA256, digest) + response.WriteHeader(http.StatusOK) + case http.MethodDelete: + if err := s.store.RemoveGPGPublicKey(repositoryID, publishKey, fingerprint); err != nil { + http.Error(response, "repository GPG key cannot be removed", http.StatusConflict) + return + } + response.WriteHeader(http.StatusNoContent) + default: + response.Header().Set("Allow", "PUT, HEAD, DELETE") + http.Error(response, "method not allowed", http.StatusMethodNotAllowed) + } +} + +func (s *Server) putGPGKey(response http.ResponseWriter, httpRequest *http.Request, + repositoryID, publishKey, fingerprint, privatePath, publicPath string, +) { + keyID := strings.ToUpper(strings.TrimSpace(httpRequest.Header.Get(nodeprotocol.HeaderGPGKeyID))) + expectedSHA := strings.ToLower(strings.TrimSpace(httpRequest.Header.Get(nodeprotocol.HeaderGPGKeySHA256))) + if !gpgKeyIDPattern.MatchString(keyID) || !digestPattern.MatchString(expectedSHA) { + http.Error(response, "repository GPG key metadata is invalid", http.StatusBadRequest) + return + } + httpRequest.Body = http.MaxBytesReader(response, httpRequest.Body, 1<<20) + content, err := io.ReadAll(httpRequest.Body) + if err != nil || len(content) == 0 || !validPublicKeyArmor(content) { + http.Error(response, "repository GPG public key is invalid", http.StatusUnprocessableEntity) + return + } + digest := sha256.Sum256(content) + actualSHA := hex.EncodeToString(digest[:]) + if actualSHA != expectedSHA { + http.Error(response, "repository GPG public key digest mismatch", http.StatusUnprocessableEntity) + return + } + storedPath, err := s.store.PutGPGPublicKey(repositoryID, publishKey, fingerprint, content) + if err != nil || storedPath != privatePath { + http.Error(response, "repository GPG public key cannot be stored", http.StatusConflict) + return + } + writeJSON(response, http.StatusOK, nodeprotocol.GPGKeyResponse{RepositoryID: repositoryID, + Fingerprint: fingerprint, KeyID: keyID, PrivatePath: privatePath, PublicPath: publicPath, + ContentSHA256: actualSHA}) +} + +func validPublicKeyArmor(content []byte) bool { + trimmed := bytes.TrimSpace(content) + return !bytes.Contains(trimmed, []byte("-----BEGIN PGP PRIVATE KEY BLOCK-----")) && + bytes.Count(trimmed, []byte("-----BEGIN PGP PUBLIC KEY BLOCK-----")) == 1 && + bytes.Count(trimmed, []byte("-----END PGP PUBLIC KEY BLOCK-----")) == 1 && + bytes.HasPrefix(trimmed, []byte("-----BEGIN PGP PUBLIC KEY BLOCK-----")) && + bytes.HasSuffix(trimmed, []byte("-----END PGP PUBLIC KEY BLOCK-----")) +} + +func (s *Server) beginUpload(response http.ResponseWriter, httpRequest *http.Request, uploadID string) { + var input nodeprotocol.BeginUploadRequest + if err := decodeJSONBody(response, httpRequest, &input); err != nil { + return + } + if input.ExpectedSize <= 0 || input.ExpectedSize > s.store.Config().MaxUploadBytes { + http.Error(response, "upload size is outside configured bounds", http.StatusUnprocessableEntity) + return + } + relative, err := s.store.CreateUpload(uploadID, input.ExpectedSize) + if err != nil { + http.Error(response, "upload cannot be created", http.StatusConflict) + return + } + writeJSON(response, http.StatusCreated, nodeprotocol.BeginUploadResponse{RelativePath: relative}) +} + +func (s *Server) appendUpload(response http.ResponseWriter, httpRequest *http.Request, uploadID string) { + offset, err := strconv.ParseInt(httpRequest.Header.Get(nodeprotocol.HeaderChunkOffset), 10, 64) + if err != nil || offset < 0 { + http.Error(response, "chunk offset is invalid", http.StatusBadRequest) + return + } + length, err := strconv.ParseInt(httpRequest.Header.Get(nodeprotocol.HeaderChunkLength), 10, 64) + if err != nil || length <= 0 || length > int64(s.store.Config().MaxChunkBytes) { + http.Error(response, "chunk length is invalid", http.StatusBadRequest) + return + } + expectedSize, err := strconv.ParseInt(httpRequest.URL.Query().Get("expected_size"), 10, 64) + if err != nil || expectedSize <= 0 || expectedSize > s.store.Config().MaxUploadBytes { + http.Error(response, "expected upload size is invalid", http.StatusBadRequest) + return + } + expectedSHA := strings.ToLower(httpRequest.Header.Get(nodeprotocol.HeaderChunkSHA256)) + if !digestPattern.MatchString(expectedSHA) { + http.Error(response, "chunk digest is invalid", http.StatusBadRequest) + return + } + content, err := io.ReadAll(io.LimitReader(httpRequest.Body, length+1)) + if err != nil || int64(len(content)) != length { + http.Error(response, "chunk body length mismatch", http.StatusBadRequest) + return + } + digest := sha256.Sum256(content) + if hex.EncodeToString(digest[:]) != expectedSHA { + http.Error(response, "chunk body digest mismatch", http.StatusUnprocessableEntity) + return + } + relative, _ := nodeprotocol.UploadRelativePath(uploadID) + next, err := s.store.Append(relative, offset, content, expectedSize) + if err != nil { + http.Error(response, "upload chunk cannot be appended", http.StatusConflict) + return + } + response.Header().Set(nodeprotocol.HeaderNextOffset, strconv.FormatInt(next, 10)) + response.WriteHeader(http.StatusNoContent) +} + +func (s *Server) completeUpload(response http.ResponseWriter, httpRequest *http.Request, uploadID string) { + var input nodeprotocol.CompleteUploadRequest + if err := decodeJSONBody(response, httpRequest, &input); err != nil { + return + } + if input.ExpectedSize <= 0 || input.ExpectedSize > s.store.Config().MaxUploadBytes || + (input.ExpectedSHA256 != "" && !digestPattern.MatchString(input.ExpectedSHA256)) { + http.Error(response, "upload completion expectation is invalid", http.StatusBadRequest) + return + } + relative, _ := nodeprotocol.UploadRelativePath(uploadID) + var digest string + var size int64 + var err error + if input.TotalChunks > 0 { + relative, size, digest, err = s.store.AssembleUpload(uploadID, input.TotalChunks, input.ExpectedSize) + } else { + digest, size, err = s.store.SHA256(relative) + } + if err != nil || size != input.ExpectedSize { + http.Error(response, "upload content is incomplete", http.StatusUnprocessableEntity) + return + } + if input.ExpectedSHA256 != "" && digest != input.ExpectedSHA256 { + http.Error(response, "upload checksum mismatch", http.StatusUnprocessableEntity) + return + } + writeJSON(response, http.StatusOK, nodeprotocol.CompleteUploadResponse{ + RelativePath: relative, SizeBytes: size, SHA256: digest}) +} + +func (s *Server) abortUpload(response http.ResponseWriter, uploadID string) { + relative, _ := nodeprotocol.UploadRelativePath(uploadID) + if _, err := s.store.MoveToTrash(relative, "aborted-uploads", uploadID); err != nil { + http.Error(response, "upload cannot be moved to trash", http.StatusConflict) + return + } + response.WriteHeader(http.StatusNoContent) +} + +func decodeJSONBody(response http.ResponseWriter, httpRequest *http.Request, destination interface{}) error { + httpRequest.Body = http.MaxBytesReader(response, httpRequest.Body, 4096) + decoder := json.NewDecoder(httpRequest.Body) + decoder.DisallowUnknownFields() + if err := decoder.Decode(destination); err != nil { + http.Error(response, "request JSON is invalid", http.StatusBadRequest) + return err + } + if err := decoder.Decode(new(interface{})); !errors.Is(err, io.EOF) { + http.Error(response, "request JSON contains trailing data", http.StatusBadRequest) + return errors.New("request JSON contains trailing data") + } + return nil +} + +func writeJSON(response http.ResponseWriter, status int, value interface{}) { + response.Header().Set("Content-Type", "application/json") + response.Header().Set("Cache-Control", "no-store") + response.WriteHeader(status) + _ = json.NewEncoder(response).Encode(value) +} + +var digestPattern = regexp.MustCompile(`^[0-9a-f]{64}$`) +var gpgKeyIDPattern = regexp.MustCompile(`^[0-9A-F]{8,16}$`) + +func verifiedClient(httpRequest *http.Request) bool { + return httpRequest.TLS != nil && len(httpRequest.TLS.VerifiedChains) > 0 +} + +func (s *Server) Handler() http.Handler { return s.handler } + +func (s *Server) Run(ctx context.Context) error { + if !s.cfg.Enabled { + return errors.New("repository node mTLS server is disabled") + } + if strings.TrimSpace(s.cfg.ListenAddress) == "" || s.cfg.ServerCert == "" || + s.cfg.ServerKey == "" || s.cfg.ClientCA == "" { + return errors.New("repository node mTLS server configuration is incomplete") + } + certificate, err := tls.LoadX509KeyPair(s.cfg.ServerCert, s.cfg.ServerKey) + if err != nil { + return fmt.Errorf("load repository node server certificate: %w", err) + } + caBytes, err := os.ReadFile(s.cfg.ClientCA) + if err != nil { + return fmt.Errorf("read repository node client CA: %w", err) + } + clientCAs := x509.NewCertPool() + if !clientCAs.AppendCertsFromPEM(caBytes) { + return errors.New("repository node client CA does not contain a certificate") + } + listener, err := net.Listen("tcp", s.cfg.ListenAddress) + if err != nil { + return fmt.Errorf("listen repository node mTLS server: %w", err) + } + tlsListener := tls.NewListener(listener, &tls.Config{Certificates: []tls.Certificate{certificate}, + ClientCAs: clientCAs, ClientAuth: tls.RequireAndVerifyClientCert, MinVersion: tls.VersionTLS12}) + httpServer := &http.Server{Handler: s.handler, + ReadHeaderTimeout: time.Duration(s.cfg.ReadTimeoutSeconds) * time.Second, + ReadTimeout: time.Duration(s.cfg.ReadTimeoutSeconds) * time.Second} + errChannel := make(chan error, 1) + go func() { errChannel <- httpServer.Serve(tlsListener) }() + select { + case <-ctx.Done(): + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if shutdownErr := httpServer.Shutdown(shutdownCtx); shutdownErr != nil { + return shutdownErr + } + err = <-errChannel + case err = <-errChannel: + } + if errors.Is(err, http.ErrServerClosed) { + return nil + } + return err +} + +func (s *Server) serveTaskResult(response http.ResponseWriter, httpRequest *http.Request) { + if !verifiedClient(httpRequest) { + http.Error(response, "verified mTLS client certificate required", http.StatusUnauthorized) + return + } + if httpRequest.Method != http.MethodGet && httpRequest.Method != http.MethodHead { + response.Header().Set("Allow", "GET, HEAD") + http.Error(response, "method not allowed", http.StatusMethodNotAllowed) + return + } + taskID := strings.TrimPrefix(httpRequest.URL.Path, nodeprotocol.TaskResultPathPrefix) + if taskID == httpRequest.URL.Path || uuid.Validate(taskID) != nil || strings.Contains(taskID, "/") { + http.NotFound(response, httpRequest) + return + } + objectID, _ := nodeprotocol.TaskResultObjectID(taskID) + if httpRequest.Header.Get(nodeprotocol.HeaderObjectID) != objectID { + http.Error(response, "result object identity mismatch", http.StatusBadRequest) + return + } + objectPath, err := s.store.Resolve(objectID) + if err != nil { + http.Error(response, "result object path is invalid", http.StatusBadRequest) + return + } + info, err := os.Lstat(objectPath) + if errors.Is(err, os.ErrNotExist) { + http.NotFound(response, httpRequest) + return + } + if err != nil || !info.Mode().IsRegular() || info.Size() < 0 || info.Size() > s.cfg.MaxResultBytes { + http.Error(response, "result object is not available", http.StatusUnprocessableEntity) + return + } + file, err := os.Open(filepath.Clean(objectPath)) + if err != nil { + http.Error(response, "result object is not available", http.StatusInternalServerError) + return + } + defer file.Close() + hash := sha256.New() + if _, err = io.Copy(hash, file); err != nil { + http.Error(response, "result object cannot be verified", http.StatusInternalServerError) + return + } + if _, err = file.Seek(0, io.SeekStart); err != nil { + http.Error(response, "result object cannot be read", http.StatusInternalServerError) + return + } + response.Header().Set(nodeprotocol.HeaderObjectID, objectID) + response.Header().Set(nodeprotocol.HeaderSHA256, hex.EncodeToString(hash.Sum(nil))) + response.Header().Set("Content-Type", "application/zstd") + response.Header().Set("Cache-Control", "no-store") + response.Header().Set("X-Content-Type-Options", "nosniff") + response.Header().Set("Content-Length", strconv.FormatInt(info.Size(), 10)) + response.WriteHeader(http.StatusOK) + if httpRequest.Method == http.MethodGet { + _, _ = io.Copy(response, file) + } +} diff --git a/manager/backend/services/footstone/repositoryworker/nodeapi/server_test.go b/manager/backend/services/footstone/repositoryworker/nodeapi/server_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d19286140452f22f074891ae86bd75bc28ef6acb --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/nodeapi/server_test.go @@ -0,0 +1,354 @@ +package nodeapi + +import ( + "bytes" + "context" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/nodeprotocol" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/tasklog" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/repositoryworker/controlplane" +) + +const nodeAPITestTaskID = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" + +type staticNodeProbe struct { + value controlplane.NodeHeartbeat + err error +} + +func (p staticNodeProbe) Probe(context.Context) (controlplane.NodeHeartbeat, error) { + return p.value, p.err +} + +func TestProbeHandlerIsLiveMTLSAndReadOnly(t *testing.T) { + store, err := storage.New(config.RepositoryConfig{Root: t.TempDir(), MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + server, err := New(config.RepositoryNodeServerConfig{}, store, staticNodeProbe{value: controlplane.NodeHeartbeat{ + FilesystemType: "xfs", CapacityBytes: 1000, FreeBytes: 700, FreeInodes: 20000, + ToolVersions: map[string]string{"rpm": "4.16"}, + RepositoryUsages: []controlplane.RepositoryUsage{{ + RepositoryID: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", LogicalBytes: 128, + PhysicalBytes: 256, PublishStatus: "READY", ObservedAt: time.Now().UTC(), + }}, + RepositoryUsageSnapshot: true, + }}) + if err != nil { + t.Fatal(err) + } + request := verifiedRequest(http.MethodGet, nodeprotocol.ProbePath) + response := httptest.NewRecorder() + server.Handler().ServeHTTP(response, request) + var value nodeprotocol.ProbeResponse + if response.Code != http.StatusOK || json.Unmarshal(response.Body.Bytes(), &value) != nil || + value.FilesystemType != "xfs" || value.FreeBytes != 700 || value.ObservedAt.IsZero() || + time.Since(value.ObservedAt) > time.Minute || len(value.RepositoryUsages) != 1 || + value.RepositoryUsages[0].PhysicalBytes != 256 || !value.RepositoryUsageSnapshot { + t.Fatalf("probe response = %d/%s", response.Code, response.Body.String()) + } + request = verifiedRequest(http.MethodDelete, nodeprotocol.ProbePath) + response = httptest.NewRecorder() + server.Handler().ServeHTTP(response, request) + if response.Code != http.StatusMethodNotAllowed { + t.Fatalf("DELETE probe response = %d", response.Code) + } + request = httptest.NewRequest(http.MethodGet, nodeprotocol.ProbePath, nil) + response = httptest.NewRecorder() + server.Handler().ServeHTTP(response, request) + if response.Code != http.StatusUnauthorized { + t.Fatalf("unverified probe response = %d", response.Code) + } +} + +func TestTaskResultHandlerIsBoundedAndReadOnly(t *testing.T) { + store, err := storage.New(config.RepositoryConfig{Root: t.TempDir(), MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + objectID, _ := nodeprotocol.TaskResultObjectID(nodeAPITestTaskID) + objectPath, err := store.Resolve(objectID) + if err != nil { + t.Fatal(err) + } + if err = os.MkdirAll(filepath.Dir(objectPath), 0750); err != nil { + t.Fatal(err) + } + if err = os.WriteFile(objectPath, []byte("result fixture"), 0440); err != nil { + t.Fatal(err) + } + server, err := New(config.RepositoryNodeServerConfig{MaxResultBytes: 1024}, store) + if err != nil { + t.Fatal(err) + } + for _, method := range []string{http.MethodGet, http.MethodHead} { + request := verifiedRequest(method, nodeprotocol.TaskResultPathPrefix+nodeAPITestTaskID) + request.Header.Set(nodeprotocol.HeaderObjectID, objectID) + response := httptest.NewRecorder() + server.Handler().ServeHTTP(response, request) + if response.Code != http.StatusOK || response.Header().Get(nodeprotocol.HeaderSHA256) == "" { + t.Fatalf("%s response = %d, headers=%v", method, response.Code, response.Header()) + } + if method == http.MethodGet && response.Body.String() != "result fixture" { + t.Fatalf("GET body = %q", response.Body.String()) + } + if method == http.MethodHead && response.Body.Len() != 0 { + t.Fatalf("HEAD returned a response body") + } + } + request := verifiedRequest(http.MethodPut, nodeprotocol.TaskResultPathPrefix+nodeAPITestTaskID) + request.Header.Set(nodeprotocol.HeaderObjectID, objectID) + response := httptest.NewRecorder() + server.Handler().ServeHTTP(response, request) + if response.Code != http.StatusMethodNotAllowed { + t.Fatalf("PUT response = %d", response.Code) + } +} + +func TestTaskLogHandlerServesMetadataAndContentReadOnly(t *testing.T) { + store, err := storage.New(config.RepositoryConfig{Root: t.TempDir(), MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + writer, err := tasklog.New(store, nodeAPITestTaskID) + if err != nil { + t.Fatal(err) + } + if err = writer.Event("INFO", "sync", "metadata downloaded", nil); err != nil { + t.Fatal(err) + } + metadata, err := writer.Close("SUCCESS", "", "") + if err != nil { + t.Fatal(err) + } + server, err := New(config.RepositoryNodeServerConfig{}, store) + if err != nil { + t.Fatal(err) + } + request := verifiedRequest(http.MethodGet, nodeprotocol.TaskLogPathPrefix+nodeAPITestTaskID+"/metadata") + response := httptest.NewRecorder() + server.Handler().ServeHTTP(response, request) + if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), metadata.SHA256) { + t.Fatalf("metadata response = %d/%s", response.Code, response.Body.String()) + } + request = verifiedRequest(http.MethodGet, nodeprotocol.TaskLogPathPrefix+nodeAPITestTaskID+"/content") + response = httptest.NewRecorder() + server.Handler().ServeHTTP(response, request) + if response.Code != http.StatusOK || response.Header().Get(nodeprotocol.HeaderSHA256) != metadata.SHA256 || + int64(response.Body.Len()) != metadata.CompressedBytes { + t.Fatalf("content response = %d/%v/%d", response.Code, response.Header(), response.Body.Len()) + } + request = verifiedRequest(http.MethodDelete, nodeprotocol.TaskLogPathPrefix+nodeAPITestTaskID+"/content") + response = httptest.NewRecorder() + server.Handler().ServeHTTP(response, request) + if response.Code != http.StatusMethodNotAllowed { + t.Fatalf("DELETE response = %d", response.Code) + } +} + +func TestUploadLifecycleRequiresExactChunkMetadata(t *testing.T) { + store, err := storage.New(config.RepositoryConfig{Root: t.TempDir(), MinimumFreeBytes: 1, + MaxUploadBytes: 1024, MaxChunkBytes: 16}) + if err != nil { + t.Fatal(err) + } + server, err := New(config.RepositoryNodeServerConfig{}, store) + if err != nil { + t.Fatal(err) + } + base := nodeprotocol.UploadPathPrefix + nodeAPITestTaskID + beginBody, _ := json.Marshal(nodeprotocol.BeginUploadRequest{ExpectedSize: 14}) + request := verifiedRequest(http.MethodPost, base+"/begin") + request.Body = io.NopCloser(bytes.NewReader(beginBody)) + response := httptest.NewRecorder() + server.Handler().ServeHTTP(response, request) + if response.Code != http.StatusCreated { + t.Fatalf("begin response = %d: %s", response.Code, response.Body.String()) + } + chunk := []byte("result fixture") + digest := sha256.Sum256(chunk) + request = verifiedRequest(http.MethodPut, base+"/content?expected_size=14") + request.Body = io.NopCloser(bytes.NewReader(chunk)) + request.Header.Set(nodeprotocol.HeaderChunkOffset, "0") + request.Header.Set(nodeprotocol.HeaderChunkLength, "14") + request.Header.Set(nodeprotocol.HeaderChunkSHA256, hex.EncodeToString(digest[:])) + response = httptest.NewRecorder() + server.Handler().ServeHTTP(response, request) + if response.Code != http.StatusNoContent || response.Header().Get(nodeprotocol.HeaderNextOffset) != "14" { + t.Fatalf("append response = %d headers=%v body=%s", response.Code, response.Header(), response.Body.String()) + } + completeBody, _ := json.Marshal(nodeprotocol.CompleteUploadRequest{ExpectedSize: 14, + ExpectedSHA256: hex.EncodeToString(digest[:])}) + request = verifiedRequest(http.MethodPost, base+"/complete") + request.Body = io.NopCloser(bytes.NewReader(completeBody)) + response = httptest.NewRecorder() + server.Handler().ServeHTTP(response, request) + if response.Code != http.StatusOK { + t.Fatalf("complete response = %d: %s", response.Code, response.Body.String()) + } + var completed nodeprotocol.CompleteUploadResponse + if err = json.Unmarshal(response.Body.Bytes(), &completed); err != nil || completed.SizeBytes != 14 || + completed.SHA256 != hex.EncodeToString(digest[:]) { + t.Fatalf("complete document/error = %#v/%v", completed, err) + } + request = verifiedRequest(http.MethodDelete, base) + response = httptest.NewRecorder() + server.Handler().ServeHTTP(response, request) + if response.Code != http.StatusNoContent { + t.Fatalf("abort response = %d: %s", response.Code, response.Body.String()) + } + if _, err = os.Stat(filepath.Join(store.Root(), "trash", "aborted-uploads", nodeAPITestTaskID, "content")); err != nil { + t.Fatalf("aborted upload was not moved to trash: %v", err) + } +} + +func TestUploadChunkRejectsChecksumMismatch(t *testing.T) { + store, err := storage.New(config.RepositoryConfig{Root: t.TempDir(), MinimumFreeBytes: 1, + MaxUploadBytes: 1024, MaxChunkBytes: 16}) + if err != nil { + t.Fatal(err) + } + server, _ := New(config.RepositoryNodeServerConfig{}, store) + base := nodeprotocol.UploadPathPrefix + nodeAPITestTaskID + beginBody, _ := json.Marshal(nodeprotocol.BeginUploadRequest{ExpectedSize: 4}) + request := verifiedRequest(http.MethodPost, base+"/begin") + request.Body = io.NopCloser(bytes.NewReader(beginBody)) + server.Handler().ServeHTTP(httptest.NewRecorder(), request) + request = verifiedRequest(http.MethodPut, base+"/content?expected_size=4") + request.Body = io.NopCloser(bytes.NewReader([]byte("data"))) + request.Header.Set(nodeprotocol.HeaderChunkOffset, "0") + request.Header.Set(nodeprotocol.HeaderChunkLength, "4") + request.Header.Set(nodeprotocol.HeaderChunkSHA256, strings.Repeat("f", 64)) + response := httptest.NewRecorder() + server.Handler().ServeHTTP(response, request) + if response.Code != http.StatusUnprocessableEntity { + t.Fatalf("bad checksum response = %d", response.Code) + } + relative, _ := nodeprotocol.UploadRelativePath(nodeAPITestTaskID) + size, err := store.Size(relative) + if err != nil || size != 0 { + t.Fatalf("rejected chunk changed upload size = %d/%v", size, err) + } +} + +func TestIndexedUploadAcceptsOutOfOrderChunksAndCompletes(t *testing.T) { + store, err := storage.New(config.RepositoryConfig{Root: t.TempDir(), MinimumFreeBytes: 1, + MaxUploadBytes: 1024, MaxChunkBytes: 16}) + if err != nil { + t.Fatal(err) + } + server, _ := New(config.RepositoryNodeServerConfig{}, store) + base := nodeprotocol.UploadPathPrefix + nodeAPITestTaskID + beginBody, _ := json.Marshal(nodeprotocol.BeginUploadRequest{ExpectedSize: 6}) + request := verifiedRequest(http.MethodPost, base+"/begin") + request.Body = io.NopCloser(bytes.NewReader(beginBody)) + response := httptest.NewRecorder() + server.Handler().ServeHTTP(response, request) + if response.Code != http.StatusCreated { + t.Fatalf("begin response = %d: %s", response.Code, response.Body.String()) + } + for _, chunk := range []struct { + index int + body string + }{{index: 1, body: "def"}, {index: 0, body: "abc"}} { + digest := sha256.Sum256([]byte(chunk.body)) + request = verifiedRequest(http.MethodPut, fmt.Sprintf("%s/chunks/%d", base, chunk.index)) + request.Body = io.NopCloser(strings.NewReader(chunk.body)) + request.Header.Set(nodeprotocol.HeaderChunkLength, "3") + request.Header.Set(nodeprotocol.HeaderChunkSHA256, hex.EncodeToString(digest[:])) + response = httptest.NewRecorder() + server.Handler().ServeHTTP(response, request) + if response.Code != http.StatusOK { + t.Fatalf("put chunk %d response = %d: %s", chunk.index, response.Code, response.Body.String()) + } + } + completeBody, _ := json.Marshal(nodeprotocol.CompleteUploadRequest{ExpectedSize: 6, TotalChunks: 2}) + request = verifiedRequest(http.MethodPost, base+"/complete") + request.Body = io.NopCloser(bytes.NewReader(completeBody)) + response = httptest.NewRecorder() + server.Handler().ServeHTTP(response, request) + if response.Code != http.StatusOK { + t.Fatalf("complete response = %d: %s", response.Code, response.Body.String()) + } + var completed nodeprotocol.CompleteUploadResponse + if err = json.Unmarshal(response.Body.Bytes(), &completed); err != nil || completed.SizeBytes != 6 || + completed.SHA256 != "bef57ec7f53a6d40beb640a780a639c83bc29ac8a9816f1fc6c5c6dcd93c4721" { + t.Fatalf("complete document/error = %#v/%v", completed, err) + } +} + +func TestTaskResultHandlerRejectsOversizedObject(t *testing.T) { + store, err := storage.New(config.RepositoryConfig{Root: t.TempDir(), MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + objectID, _ := nodeprotocol.TaskResultObjectID(nodeAPITestTaskID) + objectPath, err := store.Resolve(objectID) + if err != nil { + t.Fatal(err) + } + if err = os.MkdirAll(filepath.Dir(objectPath), 0750); err != nil { + t.Fatal(err) + } + if err = os.WriteFile(objectPath, []byte("too large"), 0440); err != nil { + t.Fatal(err) + } + server, err := New(config.RepositoryNodeServerConfig{MaxResultBytes: 4}, store) + if err != nil { + t.Fatal(err) + } + request := verifiedRequest(http.MethodGet, nodeprotocol.TaskResultPathPrefix+nodeAPITestTaskID) + request.Header.Set(nodeprotocol.HeaderObjectID, objectID) + response := httptest.NewRecorder() + server.Handler().ServeHTTP(response, request) + if response.Code != http.StatusUnprocessableEntity { + t.Fatalf("oversized response = %d", response.Code) + } +} + +func TestTaskResultHandlerRejectsUnverifiedAndArbitraryPaths(t *testing.T) { + store, err := storage.New(config.RepositoryConfig{Root: t.TempDir(), MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + server, err := New(config.RepositoryNodeServerConfig{}, store) + if err != nil { + t.Fatal(err) + } + request := httptest.NewRequest(http.MethodGet, nodeprotocol.TaskResultPathPrefix+nodeAPITestTaskID, nil) + response := httptest.NewRecorder() + server.Handler().ServeHTTP(response, request) + if response.Code != http.StatusUnauthorized { + t.Fatalf("unverified response = %d", response.Code) + } + request = verifiedRequest(http.MethodGet, nodeprotocol.TaskResultPathPrefix+nodeAPITestTaskID+"/secret") + request.Header.Set(nodeprotocol.HeaderObjectID, "../../secret") + response = httptest.NewRecorder() + server.Handler().ServeHTTP(response, request) + if response.Code != http.StatusNotFound { + body, _ := io.ReadAll(response.Body) + t.Fatalf("arbitrary path response = %d: %s", response.Code, body) + } +} + +func verifiedRequest(method, target string) *http.Request { + request := httptest.NewRequest(method, target, nil) + request.TLS = &tls.ConnectionState{VerifiedChains: [][]*x509.Certificate{{new(x509.Certificate)}}} + return request +} diff --git a/manager/backend/services/footstone/repositoryworker/publication.go b/manager/backend/services/footstone/repositoryworker/publication.go new file mode 100644 index 0000000000000000000000000000000000000000..60958ae80a9acc2e0a45046ce5ab34c90858fc8f --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/publication.go @@ -0,0 +1,308 @@ +package repositoryworker + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "sort" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" +) + +type MetadataTool interface { + Create(context.Context, string, string) error +} + +type commandMetadataTool struct{} + +type manifestPackage struct { + Name string `json:"name"` + Epoch string `json:"epoch"` + Version string `json:"version"` + Release string `json:"release"` + Architecture string `json:"architecture"` + NEVRA string `json:"nevra"` + FileName string `json:"file_name"` + Path string `json:"path"` + SHA256 string `json:"sha256"` + SizeBytes int64 `json:"size_bytes"` + Source string `json:"source"` + SourceUploadID string `json:"source_upload_id,omitempty"` + SignatureStatus string `json:"signature_status,omitempty"` + SignerKeyID string `json:"signer_key_id,omitempty"` + SignatureMessage string `json:"signature_message,omitempty"` +} + +type manifestFile struct { + Path string `json:"path"` + SHA256 string `json:"sha256"` + SizeBytes int64 `json:"size_bytes"` +} + +type publishedManifest struct { + SchemaVersion int `json:"schema_version"` + RepositoryID string `json:"repository_id"` + RevisionID string `json:"revision_id"` + RepomdSHA256 string `json:"repomd_sha256"` + Packages []manifestPackage `json:"packages"` + MetadataFiles []manifestFile `json:"metadata_files"` + PublicFiles []manifestFile `json:"public_files,omitempty"` + MetadataSigned bool `json:"metadata_signed,omitempty"` + MetadataSigningKeyRef string `json:"metadata_signing_key_ref,omitempty"` + MetadataSigningFingerprint string `json:"metadata_signing_fingerprint,omitempty"` + MetadataPublicKeyURL string `json:"metadata_public_key_url,omitempty"` +} + +func (commandMetadataTool) Create(ctx context.Context, root, revisionID string) error { + command := exec.CommandContext(ctx, "createrepo_c", "--checksum", "sha256", "--revision", revisionID, root) + if output, err := runLoggedCommand(ctx, "generate-repodata", command); err != nil { + return fmt.Errorf("createrepo_c failed: %s", truncateOutput(output)) + } + return nil +} + +func (h *Handler) publishRevision(ctx context.Context, task *po.RepositoryTask, + repository *po.SoftwareRepository, revision *po.RepositoryRevision, packages []po.RepositoryPackage, + persistExtra func(entity.Session) error, +) (published []po.RepositoryPackage, err error) { + finalRelative, published, err := h.materializePublishedRevision(ctx, task, repository, revision, packages) + if err != nil { + return nil, err + } + var previous string + var switched bool + persistErr := h.tasks.ExecuteFencedMutation(request.ExecuteRepositoryMutationRequest{ + TaskID: task.ID, WorkerID: task.WorkerID, FencingToken: task.FencingToken, + RevisionID: revision.ID, + }, func(tx entity.Session) (txErr error) { + previous, txErr = h.store.SwitchCurrent(repository.PublishKey, revision.ID) + if txErr != nil { + return txErr + } + switched = true + if txErr = h.content.CreateRevision(revision, published, tx); txErr != nil { + return txErr + } + if persistExtra != nil { + if txErr = persistExtra(tx); txErr != nil { + return txErr + } + } + return h.repositories.ActivateRevision(repository.ID, revision.ID, request.RepositoryStatusReady, tx) + }) + if persistErr != nil { + var restoreErr error + if switched { + restoreErr = h.store.RestoreCurrent(repository.PublishKey, previous) + } + h.movePublishedToTrash(finalRelative, revision.ID) + if restoreErr != nil { + return nil, fmt.Errorf("persist revision: %v; restore current: %w", persistErr, restoreErr) + } + return nil, persistErr + } + return published, nil +} + +func (h *Handler) materializePublishedRevision(ctx context.Context, task *po.RepositoryTask, + repository *po.SoftwareRepository, revision *po.RepositoryRevision, packages []po.RepositoryPackage, +) (string, []po.RepositoryPackage, error) { + finalRelative, err := h.store.PublicRevisionRelativePath(repository.PublishKey, revision.ID) + if err != nil { + return "", nil, err + } + finalPath, err := h.store.Resolve(finalRelative) + if err != nil { + return "", nil, err + } + stagingRelative := filepath.ToSlash(filepath.Join("staging", "publish", task.ID, revision.ID)) + stagingPath, err := h.store.Resolve(stagingRelative) + if err != nil { + return "", nil, err + } + taskStaging, _ := h.store.Resolve(filepath.ToSlash(filepath.Join("staging", "publish", task.ID))) + if err = os.RemoveAll(taskStaging); err != nil { + return "", nil, err + } + if err = h.store.EnsureFreeSpace(totalPackageSize(packages) + h.store.Config().MetadataReserve); err != nil { + return "", nil, err + } + if err = os.MkdirAll(stagingPath, 0755); err != nil { + return "", nil, err + } + published := make([]po.RepositoryPackage, len(packages)) + copy(published, packages) + for index := range published { + source, resolveErr := h.store.Resolve(published[index].RelativePath) + if resolveErr != nil { + return "", nil, resolveErr + } + name := safePackageFileName(published[index].SHA256, published[index].FileName) + if err = linkOrCopy(source, filepath.Join(stagingPath, name)); err != nil { + return "", nil, err + } + published[index].RelativePath = filepath.ToSlash(filepath.Join(finalRelative, name)) + } + if err = h.metadata.Create(ctx, stagingPath, revision.ID); err != nil { + return "", nil, err + } + if err = h.signRepositoryMetadata(ctx, repository, revision, stagingPath); err != nil { + return "", nil, err + } + repomd := filepath.Join(stagingPath, "repodata", "repomd.xml") + repomdSHA, _, err := fileSHA256(repomd) + if err != nil { + return "", nil, fmt.Errorf("verify generated repomd.xml: %w", err) + } + manifestPackages := make([]manifestPackage, 0, len(published)) + for _, item := range published { + manifestPackages = append(manifestPackages, manifestPackage{ + Name: item.Name, Epoch: item.Epoch, Version: item.Version, Release: item.Release, + Architecture: item.Architecture, NEVRA: item.NEVRA, FileName: item.FileName, + Path: filepath.Base(item.RelativePath), SHA256: item.SHA256, SizeBytes: item.SizeBytes, + Source: item.Source, SourceUploadID: item.SourceUploadID, + SignatureStatus: item.SignatureStatus, SignerKeyID: item.SignerKeyID, + SignatureMessage: item.SignatureMessage, + }) + } + sort.Slice(manifestPackages, func(i, j int) bool { return manifestPackages[i].NEVRA < manifestPackages[j].NEVRA }) + metadataFiles, err := hashPublishedMetadata(stagingPath) + if err != nil { + return "", nil, err + } + publicFiles, err := hashPublishedPublicFiles(stagingPath) + if err != nil { + return "", nil, err + } + manifestBytes, err := json.MarshalIndent(publishedManifest{ + SchemaVersion: 3, RepositoryID: repository.ID, RevisionID: revision.ID, + RepomdSHA256: repomdSHA, Packages: manifestPackages, MetadataFiles: metadataFiles, + PublicFiles: publicFiles, MetadataSigned: revision.MetadataSigned, + MetadataSigningKeyRef: revision.MetadataSigningKeyRef, + MetadataSigningFingerprint: revision.MetadataSigningFingerprint, + MetadataPublicKeyURL: revision.MetadataPublicKeyURL, + }, "", " ") + if err != nil { + return "", nil, err + } + manifestBytes = append(manifestBytes, '\n') + if err = os.WriteFile(filepath.Join(stagingPath, "manifest.json"), manifestBytes, 0444); err != nil { + return "", nil, err + } + if err = os.MkdirAll(filepath.Dir(finalPath), 0755); err != nil { + return "", nil, err + } + if err = os.Rename(stagingPath, finalPath); err != nil { + return "", nil, fmt.Errorf("finalize published revision: %w", err) + } + if err = makePublishedTreeReadOnly(finalPath); err != nil { + h.movePublishedToTrash(finalRelative, revision.ID) + return "", nil, err + } + digest := sha256.Sum256(manifestBytes) + now := h.now() + revision.RelativePath = finalRelative + revision.RepomdSHA256 = repomdSHA + revision.SnapshotManifestSHA256 = hex.EncodeToString(digest[:]) + revision.Status = request.RepositoryRevisionStatusPublished + revision.PublishedAt = &now + return finalRelative, published, nil +} + +func hashPublishedPublicFiles(root string) ([]manifestFile, error) { + keysRoot := filepath.Join(root, "keys") + if _, err := os.Stat(keysRoot); os.IsNotExist(err) { + return []manifestFile{}, nil + } else if err != nil { + return nil, err + } + values := make([]manifestFile, 0) + err := filepath.Walk(keysRoot, func(path string, info os.FileInfo, walkErr error) error { + if walkErr != nil { + return walkErr + } + if info.IsDir() { + return nil + } + if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 { + return fmt.Errorf("published key directory contains a non-regular file") + } + digest, size, hashErr := fileSHA256(path) + if hashErr != nil { + return hashErr + } + relative, relativeErr := filepath.Rel(root, path) + if relativeErr != nil { + return relativeErr + } + values = append(values, manifestFile{Path: filepath.ToSlash(relative), SHA256: digest, SizeBytes: size}) + return nil + }) + sort.Slice(values, func(i, j int) bool { return values[i].Path < values[j].Path }) + return values, err +} + +func hashPublishedMetadata(root string) ([]manifestFile, error) { + metadataRoot := filepath.Join(root, "repodata") + values := make([]manifestFile, 0) + err := filepath.Walk(metadataRoot, func(path string, info os.FileInfo, walkErr error) error { + if walkErr != nil { + return walkErr + } + if info.IsDir() { + return nil + } + if !info.Mode().IsRegular() { + return fmt.Errorf("repodata contains a non-regular file") + } + digest, size, hashErr := fileSHA256(path) + if hashErr != nil { + return hashErr + } + relative, relativeErr := filepath.Rel(root, path) + if relativeErr != nil { + return relativeErr + } + values = append(values, manifestFile{Path: filepath.ToSlash(relative), SHA256: digest, SizeBytes: size}) + return nil + }) + sort.Slice(values, func(i, j int) bool { return values[i].Path < values[j].Path }) + return values, err +} + +func makePublishedTreeReadOnly(root string) error { + return filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.Mode()&os.ModeSymlink != 0 { + return fmt.Errorf("published revision contains a symlink") + } + if info.IsDir() { + return os.Chmod(path, 0555) + } + if !info.Mode().IsRegular() { + return fmt.Errorf("published revision contains a non-regular file") + } + return os.Chmod(path, 0444) + }) +} + +func (h *Handler) movePublishedToTrash(relative, revisionID string) { + path, err := h.store.Resolve(relative) + if err != nil { + return + } + destination, err := h.store.Resolve(filepath.ToSlash(filepath.Join("trash", "failed-published", revisionID))) + if err == nil { + _ = os.MkdirAll(filepath.Dir(destination), 0750) + _ = os.Rename(path, destination) + } +} diff --git a/manager/backend/services/footstone/repositoryworker/remote_delete_executor.go b/manager/backend/services/footstone/repositoryworker/remote_delete_executor.go new file mode 100644 index 0000000000000000000000000000000000000000..ddc7a048c577e834bc758bc8cfd5543c7559bf6b --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/remote_delete_executor.go @@ -0,0 +1,172 @@ +package repositoryworker + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "strings" + "time" + + "github.com/google/uuid" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + repositorynodepb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repositorynode" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/taskresult" +) + +type RemoteDeleteExecutor struct { + store *storage.Store + rpm RPMTool + metadata MetadataTool + now func() time.Time +} + +func NewRemoteDeleteExecutor(store *storage.Store) (*RemoteDeleteExecutor, error) { + if store == nil { + return nil, fmt.Errorf("remote delete executor requires repository storage") + } + return &RemoteDeleteExecutor{store: store, rpm: commandRPMTool{}, metadata: commandMetadataTool{}, + now: func() time.Time { return time.Now().UTC() }}, nil +} + +func (e *RemoteDeleteExecutor) Execute(ctx context.Context, + spec *repositorynodepb.RepositoryTaskSpec, +) (RemoteOutcome, error) { + if spec == nil || spec.GetTaskType() != repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_DELETE_PACKAGE { + return RemoteOutcome{}, FailedExecution("UNSUPPORTED_TASK_TYPE", + "remote delete executor only accepts DELETE_PACKAGE tasks") + } + if uuid.Validate(spec.GetTaskId()) != nil || uuid.Validate(spec.GetRepositoryId()) != nil || + uuid.Validate(spec.GetStorageNodeId()) != nil || uuid.Validate(spec.GetActiveRevisionId()) != nil || + strings.TrimSpace(spec.GetPublishKey()) == "" { + return RemoteOutcome{}, FailedExecution("INVALID_TASK_SPEC", "remote delete task identity is invalid") + } + payload, err := decodeDeletePackagePayload(spec.GetPayloadJson()) + if err != nil { + return RemoteOutcome{}, FailedExecution("INVALID_DELETE_PAYLOAD", err.Error()) + } + if payload.RevisionID != "" && payload.RevisionID != spec.GetActiveRevisionId() { + return RemoteOutcome{}, FailedExecution("REVISION_CHANGED", + "active repository revision changed after package deletion preview") + } + repository := &po.SoftwareRepository{ID: spec.GetRepositoryId(), PublishKey: spec.GetPublishKey(), + Architecture: spec.GetArchitecture(), GPGPolicy: spec.GetGpgPolicy(), + PublishBaseURLSnapshot: spec.GetPublishBaseUrl()} + applyMetadataSigningReference(repository, spec.GetMetadataSigning()) + parent, packages, err := loadRemoteActiveInventory(ctx, e.store, e.rpm, spec, repository) + if err != nil { + return RemoteOutcome{}, FailedExecution("ACTIVE_REVISION_INVENTORY_INVALID", err.Error()) + } + if parent == nil { + return RemoteOutcome{}, FailedExecution("REVISION_NOT_FOUND", "repository has no active revision") + } + deleting := make(map[string]struct{}, len(payload.NEVRAs)) + for _, nevra := range payload.NEVRAs { + deleting[nevra] = struct{}{} + } + remaining := make([]po.RepositoryPackage, 0, len(packages)) + found := make(map[string]struct{}, len(deleting)) + for _, item := range packages { + if _, ok := deleting[item.NEVRA]; ok { + found[item.NEVRA] = struct{}{} + continue + } + remaining = append(remaining, item) + } + if len(found) != len(deleting) { + return RemoteOutcome{}, FailedExecution("PACKAGE_NOT_FOUND", "one or more selected NEVRAs do not exist") + } + task := &po.RepositoryTask{ID: spec.GetTaskId(), RepositoryID: spec.GetRepositoryId(), + StorageNodeID: spec.GetStorageNodeId(), PayloadJSON: spec.GetPayloadJson(), + WorkerID: "remote-data-plane", FencingToken: spec.GetFencingToken()} + handler := &Handler{store: e.store, rpm: e.rpm, metadata: e.metadata, now: e.now} + revision, publishedPackages, privateRelative, err := handler.materializeDeleteRevision( + task, repository, parent, remaining) + if err != nil { + return RemoteOutcome{}, FailedExecution("REVISION_BUILD_FAILED", err.Error()) + } + finalRelative, publishedPackages, err := handler.materializePublishedRevision( + ctx, task, repository, revision, publishedPackages) + if err != nil { + handler.moveRevisionToTrash(privateRelative, revision.ID) + return RemoteOutcome{}, FailedExecution("REVISION_PUBLISH_FAILED", err.Error()) + } + resultObjectID, resultObjectSHA, err := writeRemoteResultObject(e.store, task, revision, + publishedPackages, taskresult.WriteOptions{CompletionStatus: request.RepositoryTaskStatusSuccess, + AcceptedCount: len(publishedPackages)}) + if err != nil { + handler.movePublishedToTrash(finalRelative, revision.ID) + handler.moveRevisionToTrash(privateRelative, revision.ID) + return RemoteOutcome{}, FailedExecution("TASK_RESULT_WRITE_FAILED", err.Error()) + } + return RemoteOutcome{RevisionID: revision.ID, ManifestSHA256: revision.SnapshotManifestSHA256, + ResultObjectID: resultObjectID, ResultObjectSHA256: resultObjectSHA, + PackageCount: int64(revision.PackageCount), TotalBytes: revision.TotalBytes, + CompletionStatus: repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_SUCCESS}, nil +} + +func (e *RemoteDeleteExecutor) Activate(_ context.Context, spec *repositorynodepb.RepositoryTaskSpec, + outcome RemoteOutcome, +) error { + if spec == nil || uuid.Validate(outcome.RevisionID) != nil { + return fmt.Errorf("remote delete activation identity is invalid") + } + if _, err := e.store.SwitchCurrent(spec.GetPublishKey(), outcome.RevisionID); err != nil { + return err + } + return e.store.VerifyPublishedCurrent(spec.GetPublishKey(), outcome.RevisionID) +} + +type deletePackagePayload struct { + NEVRA string `json:"nevra,omitempty"` + NEVRAs []string `json:"nevras,omitempty"` + RevisionID string `json:"revision_id,omitempty"` + PreviewID string `json:"preview_id,omitempty"` + PreviewDigest string `json:"preview_digest,omitempty"` + RepositorySet json.RawMessage `json:"repository_set,omitempty"` +} + +func decodeDeletePackagePayload(payload string) (deletePackagePayload, error) { + var value deletePackagePayload + decoder := json.NewDecoder(strings.NewReader(payload)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&value); err != nil { + return value, fmt.Errorf("decode delete package payload: %w", err) + } + if decoder.Decode(new(interface{})) != io.EOF { + return value, errors.New("delete package payload contains trailing JSON data") + } + value.NEVRA = strings.TrimSpace(value.NEVRA) + if len(value.NEVRAs) == 0 && value.NEVRA != "" { + value.NEVRAs = []string{value.NEVRA} + } + seen := make(map[string]struct{}, len(value.NEVRAs)) + normalized := make([]string, 0, len(value.NEVRAs)) + for _, nevra := range value.NEVRAs { + nevra = strings.TrimSpace(nevra) + if nevra == "" { + return value, errors.New("delete package payload contains an empty NEVRA") + } + if _, ok := seen[nevra]; ok { + continue + } + seen[nevra] = struct{}{} + normalized = append(normalized, nevra) + } + if len(normalized) == 0 || len(normalized) > 100 { + return value, errors.New("delete package payload must contain between 1 and 100 NEVRAs") + } + value.NEVRAs = normalized + value.RevisionID = strings.TrimSpace(value.RevisionID) + if value.RevisionID != "" && uuid.Validate(value.RevisionID) != nil { + return value, errors.New("delete package payload revision ID is invalid") + } + return value, nil +} + +var _ RemoteExecutor = (*RemoteDeleteExecutor)(nil) diff --git a/manager/backend/services/footstone/repositoryworker/remote_delete_executor_test.go b/manager/backend/services/footstone/repositoryworker/remote_delete_executor_test.go new file mode 100644 index 0000000000000000000000000000000000000000..f2d4b7a906d48b40a109aeb969a3c9f734dbcf6a --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/remote_delete_executor_test.go @@ -0,0 +1,219 @@ +package repositoryworker + +import ( + "context" + "encoding/json" + "errors" + "os" + "path/filepath" + "testing" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + repositorynodepb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repositorynode" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/taskresult" +) + +const remoteDeleteActiveRevisionID = "abababab-abab-4bab-8bab-abababababab" + +func TestRemoteDeleteExecutorBuildsAndActivatesRevision(t *testing.T) { + root := t.TempDir() + t.Cleanup(func() { makeRemoteTestTreeWritable(root) }) + store, err := storage.New(config.RepositoryConfig{Root: root, MetadataReserve: 1, MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + publishKey := "oc9-private-delete" + deletedNEVRA := "ocm-delete-me-0:1.0-1.noarch" + writeRemoteDeleteActiveRevision(t, store, publishKey, deletedNEVRA) + if _, err = store.SwitchCurrent(publishKey, remoteDeleteActiveRevisionID); err != nil { + t.Fatal(err) + } + payload, _ := json.Marshal(map[string]interface{}{"nevras": []string{deletedNEVRA}, + "revision_id": remoteDeleteActiveRevisionID}) + spec := &repositorynodepb.RepositoryTaskSpec{TaskId: remoteTestTaskID, + RepositoryId: remoteTestRepoID, StorageNodeId: remoteTestNodeID, + TaskType: repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_DELETE_PACKAGE, + FencingToken: 14, PayloadJson: string(payload), PublishKey: publishKey, + ActiveRevisionId: remoteDeleteActiveRevisionID, + Architecture: request.RepositoryArchitectureX8664, + GpgPolicy: request.RepositoryGPGDisabled} + metadata := &countingDeleteMetadata{delegate: remoteFakeMetadata{}} + executor := &RemoteDeleteExecutor{store: store, rpm: remoteDeleteFakeRPM{}, + metadata: metadata, now: testNow} + outcome, err := executor.Execute(context.Background(), spec) + if err != nil { + t.Fatal(err) + } + current, err := os.Readlink(filepath.Join(root, "public", publishKey, "current")) + if err != nil || filepath.ToSlash(current) != "revisions/"+remoteDeleteActiveRevisionID { + t.Fatalf("delete changed current before activation: target=%q error=%v", current, err) + } + resultPath, err := store.Resolve(outcome.ResultObjectID) + if err != nil { + t.Fatal(err) + } + result, err := os.Open(resultPath) + if err != nil { + t.Fatal(err) + } + document, parseErr := taskresult.ParseZstd(result, taskresult.Expectation{TaskID: spec.GetTaskId(), + RepositoryID: spec.GetRepositoryId(), StorageNodeID: spec.GetStorageNodeId(), + RevisionID: outcome.RevisionID, PublishKey: publishKey, ManifestSHA256: outcome.ManifestSHA256, + Kind: request.RepositoryRevisionKindDeletePackage, Architecture: spec.GetArchitecture(), + PackageCount: 1, TotalBytes: int64(len("keep-rpm")), + CompletionStatus: request.RepositoryTaskStatusSuccess}) + _ = result.Close() + if parseErr != nil { + t.Fatal(parseErr) + } + if len(document.Packages) != 1 || document.Packages[0].NEVRA != "ocm-keep-me-0:1.0-1.noarch" { + t.Fatalf("delete result packages = %#v", document.Packages) + } + if metadata.calls != 1 { + t.Fatalf("batch delete rebuilt repodata %d times, want exactly once", metadata.calls) + } + if err = executor.Activate(context.Background(), spec, outcome); err != nil { + t.Fatal(err) + } + current, err = os.Readlink(filepath.Join(root, "public", publishKey, "current")) + if err != nil || filepath.ToSlash(current) != "revisions/"+outcome.RevisionID { + t.Fatalf("delete activation target=%q error=%v", current, err) + } +} + +func TestRemoteDeleteExecutorRejectsChangedPreviewRevision(t *testing.T) { + root := t.TempDir() + store, err := storage.New(config.RepositoryConfig{Root: root, MetadataReserve: 1, MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + publishKey := "oc9-private-delete" + deletedNEVRA := "ocm-delete-me-0:1.0-1.noarch" + writeRemoteDeleteActiveRevision(t, store, publishKey, deletedNEVRA) + payload, _ := json.Marshal(map[string]interface{}{"nevras": []string{deletedNEVRA}, + "revision_id": "cdcdcdcd-cdcd-4dcd-8dcd-cdcdcdcdcdcd"}) + spec := &repositorynodepb.RepositoryTaskSpec{TaskId: remoteTestTaskID, + RepositoryId: remoteTestRepoID, StorageNodeId: remoteTestNodeID, + TaskType: repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_DELETE_PACKAGE, + PayloadJson: string(payload), PublishKey: publishKey, ActiveRevisionId: remoteDeleteActiveRevisionID} + executor := &RemoteDeleteExecutor{store: store, rpm: remoteDeleteFakeRPM{}, + metadata: remoteFakeMetadata{}, now: testNow} + _, executeErr := executor.Execute(context.Background(), spec) + var failure *ExecutionError + if !errors.As(executeErr, &failure) || failure.Code != "REVISION_CHANGED" { + t.Fatalf("changed revision error = %v", executeErr) + } +} + +func TestRemoteDeleteExecutorMissingPackageKeepsActiveRevision(t *testing.T) { + root := t.TempDir() + t.Cleanup(func() { makeRemoteTestTreeWritable(root) }) + store, err := storage.New(config.RepositoryConfig{Root: root, MetadataReserve: 1, MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + publishKey := "oc9-private-delete" + writeRemoteDeleteActiveRevision(t, store, publishKey, "ocm-delete-me-0:1.0-1.noarch") + if _, err = store.SwitchCurrent(publishKey, remoteDeleteActiveRevisionID); err != nil { + t.Fatal(err) + } + payload, _ := json.Marshal(map[string]string{"nevra": "missing-0:1.0-1.noarch"}) + spec := &repositorynodepb.RepositoryTaskSpec{TaskId: remoteTestTaskID, + RepositoryId: remoteTestRepoID, StorageNodeId: remoteTestNodeID, + TaskType: repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_DELETE_PACKAGE, + PayloadJson: string(payload), PublishKey: publishKey, + ActiveRevisionId: remoteDeleteActiveRevisionID, + Architecture: request.RepositoryArchitectureX8664} + executor := &RemoteDeleteExecutor{store: store, rpm: remoteDeleteFakeRPM{}, + metadata: remoteFakeMetadata{}, now: testNow} + _, executeErr := executor.Execute(context.Background(), spec) + var failure *ExecutionError + if !errors.As(executeErr, &failure) || failure.Code != "PACKAGE_NOT_FOUND" { + t.Fatalf("missing package error = %v", executeErr) + } + current, err := os.Readlink(filepath.Join(root, "public", publishKey, "current")) + if err != nil || filepath.ToSlash(current) != "revisions/"+remoteDeleteActiveRevisionID { + t.Fatalf("failed delete changed current: target=%q error=%v", current, err) + } +} + +func writeRemoteDeleteActiveRevision(t *testing.T, store *storage.Store, publishKey, deletedNEVRA string) { + t.Helper() + relative, err := store.PublicRevisionRelativePath(publishKey, remoteDeleteActiveRevisionID) + if err != nil { + t.Fatal(err) + } + root, err := store.Resolve(relative) + if err != nil { + t.Fatal(err) + } + if err = os.MkdirAll(root, 0755); err != nil { + t.Fatal(err) + } + values := []struct { + name, body, nevra string + }{ + {name: "delete.rpm", body: "delete-rpm", nevra: deletedNEVRA}, + {name: "keep.rpm", body: "keep-rpm", nevra: "ocm-keep-me-0:1.0-1.noarch"}, + } + manifest := publishedManifest{SchemaVersion: 3, RepositoryID: remoteTestRepoID, + RevisionID: remoteDeleteActiveRevisionID} + for _, value := range values { + path := filepath.Join(root, value.name) + if err = os.WriteFile(path, []byte(value.body), 0644); err != nil { + t.Fatal(err) + } + digest, size, hashErr := fileSHA256(path) + if hashErr != nil { + t.Fatal(hashErr) + } + name := "ocm-keep-me" + if value.body == "delete-rpm" { + name = "ocm-delete-me" + } + manifest.Packages = append(manifest.Packages, manifestPackage{Name: name, Epoch: "0", + Version: "1.0", Release: "1", Architecture: request.RepositoryArchitectureNoarch, + NEVRA: value.nevra, FileName: value.name, Path: value.name, SHA256: digest, + SizeBytes: size, Source: request.RepositoryPackageSourceUpload, + SourceUploadID: remoteUploadID}) + } + content, err := json.Marshal(manifest) + if err != nil { + t.Fatal(err) + } + if err = os.WriteFile(filepath.Join(root, "manifest.json"), content, 0644); err != nil { + t.Fatal(err) + } +} + +type remoteDeleteFakeRPM struct{} + +type countingDeleteMetadata struct { + delegate MetadataTool + calls int +} + +func (m *countingDeleteMetadata) Create(ctx context.Context, root, revisionID string) error { + m.calls++ + return m.delegate.Create(ctx, root, revisionID) +} + +func (remoteDeleteFakeRPM) Inspect(_ context.Context, path string) (RPMInfo, error) { + content, err := os.ReadFile(path) + if err != nil { + return RPMInfo{}, err + } + switch string(content) { + case "delete-rpm": + return RPMInfo{Name: "ocm-delete-me", Epoch: "0", Version: "1.0", Release: "1", + Architecture: request.RepositoryArchitectureNoarch}, nil + case "keep-rpm": + return RPMInfo{Name: "ocm-keep-me", Epoch: "0", Version: "1.0", Release: "1", + Architecture: request.RepositoryArchitectureNoarch}, nil + default: + return RPMInfo{}, errors.New("unexpected RPM fixture") + } +} diff --git a/manager/backend/services/footstone/repositoryworker/remote_executor.go b/manager/backend/services/footstone/repositoryworker/remote_executor.go new file mode 100644 index 0000000000000000000000000000000000000000..ce95c728499a39c7f5c1bc37f7db657b34e60969 --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/remote_executor.go @@ -0,0 +1,98 @@ +package repositoryworker + +import ( + "context" + "fmt" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + repositorynodepb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repositorynode" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/repositoryworker/controlplane" +) + +type RemoteExecutorRouter struct { + sync *RemoteSyncExecutor + upload *RemoteUploadExecutor + delete *RemoteDeleteExecutor + migration *RemoteMigrationExecutor + control controlplane.Plane + workerID string +} + +func NewRemoteExecutor(store *storage.Store, control controlplane.Plane, workerID string) (*RemoteExecutorRouter, error) { + if control == nil || workerID == "" { + return nil, fmt.Errorf("remote repository executor requires a control plane and worker id") + } + syncExecutor, err := NewRemoteSyncExecutor(store) + if err != nil { + return nil, err + } + uploadExecutor, err := NewRemoteUploadExecutor(store) + if err != nil { + return nil, err + } + deleteExecutor, err := NewRemoteDeleteExecutor(store) + if err != nil { + return nil, err + } + migrationExecutor, err := NewRemoteMigrationExecutor(store) + if err != nil { + return nil, err + } + return &RemoteExecutorRouter{sync: syncExecutor, upload: uploadExecutor, delete: deleteExecutor, + migration: migrationExecutor, control: control, workerID: workerID}, nil +} + +func (e *RemoteExecutorRouter) Execute(ctx context.Context, spec *repositorynodepb.RepositoryTaskSpec) ( + RemoteOutcome, error, +) { + if spec == nil { + return RemoteOutcome{}, FailedExecution("INVALID_TASK_SPEC", "repository task spec is required") + } + switch spec.GetTaskType() { + case repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_SYNC: + return e.sync.Execute(ctx, spec) + case repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_UPLOAD: + return e.upload.Execute(ctx, spec) + case repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_DELETE_PACKAGE: + return e.delete.Execute(ctx, spec) + case repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_MIGRATE_REPOSITORY: + return e.migration.Execute(ctx, spec) + case repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_DISTRIBUTE: + result, err := e.control.ExecuteDistributionTask(ctx, controlplane.DistributionExecution{ + TaskID: spec.GetTaskId(), WorkerID: e.workerID, FencingToken: spec.GetFencingToken(), + }) + if err != nil { + return RemoteOutcome{}, err + } + return RemoteOutcome{CompletionStatus: result.Status, ErrorCode: result.ErrorCode, + ErrorMessage: result.ErrorMessage}, nil + default: + return RemoteOutcome{}, FailedExecution("UNSUPPORTED_TASK_TYPE", + fmt.Sprintf("remote repository worker does not handle %s", spec.GetTaskType().String())) + } +} + +func (e *RemoteExecutorRouter) Activate(ctx context.Context, spec *repositorynodepb.RepositoryTaskSpec, + outcome RemoteOutcome, +) error { + if spec == nil { + return fmt.Errorf("repository task spec is required") + } + switch spec.GetTaskType() { + case repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_SYNC: + return e.sync.Activate(ctx, spec, outcome) + case repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_UPLOAD: + return e.upload.Activate(ctx, spec, outcome) + case repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_DELETE_PACKAGE: + return e.delete.Activate(ctx, spec, outcome) + case repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_MIGRATE_REPOSITORY: + return e.migration.Activate(ctx, spec, outcome) + case repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_DISTRIBUTE: + return nil + default: + return fmt.Errorf("unsupported remote repository activation type %s", spec.GetTaskType().String()) + } +} + +var _ RemoteExecutor = (*RemoteExecutorRouter)(nil) diff --git a/manager/backend/services/footstone/repositoryworker/remote_gpg_integration_test.go b/manager/backend/services/footstone/repositoryworker/remote_gpg_integration_test.go new file mode 100644 index 0000000000000000000000000000000000000000..56819d1258ad4522aab72376f03c34e5498b4151 --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/remote_gpg_integration_test.go @@ -0,0 +1,197 @@ +//go:build integration + +package repositoryworker + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "errors" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/google/uuid" + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/armor" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + repositorynodepb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repositorynode" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + appconfig "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/taskresult" +) + +func TestRemoteUploadExecutorGPGKeyReferencesAndQuarantine(t *testing.T) { + publicKeyPath := os.Getenv("OCM_REPOSITORY_GPG_PUBLIC_KEY") + signedRPM := os.Getenv("OCM_REPOSITORY_GPG_SIGNED_RPM") + unsignedRPM := os.Getenv("OCM_REPOSITORY_GPG_UNSIGNED_RPM") + unauthorizedRPM := os.Getenv("OCM_REPOSITORY_GPG_UNAUTHORIZED_RPM") + if publicKeyPath == "" || signedRPM == "" || unsignedRPM == "" || unauthorizedRPM == "" { + t.Skip("repository GPG fixture environment is not configured") + } + for _, path := range []string{publicKeyPath, signedRPM, unsignedRPM, unauthorizedRPM} { + if info, err := os.Stat(path); err != nil || !info.Mode().IsRegular() || info.Size() == 0 { + t.Fatalf("invalid repository GPG fixture %s: %v", path, err) + } + } + + root := t.TempDir() + t.Cleanup(func() { makeRemoteTestTreeWritable(root) }) + store, err := storage.New(appconfig.RepositoryConfig{Root: root, MinimumFreeBytes: 1, + MetadataReserve: 1, MaxUploadBytes: 64 << 20, MaxExpandedBytes: 128 << 20, + MaxArchiveFiles: 100, MaxSyncBytes: 128 << 20, MaxSyncPackages: 100}) + if err != nil { + t.Fatal(err) + } + executor, err := NewRemoteUploadExecutor(store) + if err != nil { + t.Fatal(err) + } + publicKey, err := os.ReadFile(publicKeyPath) + if err != nil { + t.Fatal(err) + } + fingerprint, keyID := remoteGPGKeyIdentity(t, publicKey) + repositoryID := uuid.NewString() + storageNodeID := uuid.NewString() + publishKey := "remote-gpg-" + repositoryID[:8] + privatePath, err := store.PutGPGPublicKey(repositoryID, publishKey, fingerprint, publicKey) + if err != nil { + t.Fatal(err) + } + keyReferences := []*repositorynodepb.RepositoryGPGKeyReference{{ + KeyId: keyID, Fingerprint: fingerprint, PublicKeyPath: privatePath, + }} + + positiveSpec := remoteGPGUploadSpec(t, store, repositoryID, storageNodeID, publishKey, + keyReferences, signedRPM, "") + positive, err := executor.Execute(context.Background(), positiveSpec) + if err != nil || positive.CompletionStatus != repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_SUCCESS || + positive.RevisionID == "" { + t.Fatalf("authorized remote GPG upload = %#v/%v", positive, err) + } + if err = executor.Activate(context.Background(), positiveSpec, positive); err != nil { + t.Fatal(err) + } + publishedRelative, err := store.PublicRevisionRelativePath(publishKey, positive.RevisionID) + if err != nil { + t.Fatal(err) + } + repomdPath, err := store.Resolve(filepath.ToSlash(filepath.Join(publishedRelative, "repodata", "repomd.xml"))) + if err != nil { + t.Fatal(err) + } + activeRepomdSHA, _, err := fileSHA256(repomdPath) + if err != nil { + t.Fatal(err) + } + + negativeCases := []struct { + name, fixture, reason string + }{ + {name: "unsigned", fixture: unsignedRPM, reason: request.RepositoryQuarantineUnsignedRPM}, + {name: "unauthorized", fixture: unauthorizedRPM, reason: request.RepositoryQuarantineUnauthorizedKey}, + } + for _, test := range negativeCases { + t.Run(test.name, func(t *testing.T) { + spec := remoteGPGUploadSpec(t, store, repositoryID, storageNodeID, publishKey, + keyReferences, test.fixture, positive.RevisionID) + outcome, executeErr := executor.Execute(context.Background(), spec) + var failure *ExecutionError + if !errors.As(executeErr, &failure) || failure.Code != "ALL_PACKAGES_REJECTED" || + outcome.FailureResultObjectID == "" || outcome.QuarantineCount != 1 { + t.Fatalf("negative remote GPG upload = %#v/%v", outcome, executeErr) + } + resultPath, resolveErr := store.Resolve(outcome.FailureResultObjectID) + if resolveErr != nil { + t.Fatal(resolveErr) + } + resultFile, openErr := os.Open(resultPath) + if openErr != nil { + t.Fatal(openErr) + } + document, parseErr := taskresult.ParseFailureZstd(resultFile, taskresult.FailureExpectation{ + TaskID: spec.GetTaskId(), RepositoryID: repositoryID, StorageNodeID: storageNodeID, + UploadID: remoteGPGUploadID(t, spec), ErrorCode: failure.Code, QuarantineCount: 1, + }) + _ = resultFile.Close() + if parseErr != nil || len(document.Quarantine) != 1 || + document.Quarantine[0].ReasonCode != test.reason { + t.Fatalf("negative remote GPG result = %#v/%v, want reason %s", document, parseErr, test.reason) + } + if verifyErr := store.VerifyPublishedCurrent(publishKey, positive.RevisionID); verifyErr != nil { + t.Fatalf("negative upload changed active revision: %v", verifyErr) + } + gotSHA, _, hashErr := fileSHA256(repomdPath) + if hashErr != nil || gotSHA != activeRepomdSHA { + t.Fatalf("negative upload changed repomd: sha=%s want=%s err=%v", gotSHA, activeRepomdSHA, hashErr) + } + }) + } + t.Logf("remote_gpg_upload=PASS repository=%s node=%s revision=%s key_id=%s fingerprint=%s repomd_sha256=%s", + repositoryID, storageNodeID, positive.RevisionID, keyID, fingerprint, activeRepomdSHA) +} + +func remoteGPGUploadSpec(t *testing.T, store *storage.Store, repositoryID, storageNodeID, publishKey string, + keys []*repositorynodepb.RepositoryGPGKeyReference, fixturePath, activeRevisionID string, +) *repositorynodepb.RepositoryTaskSpec { + t.Helper() + uploadID := uuid.NewString() + relative := store.UploadRelativePath(uploadID) + path, err := store.Resolve(relative) + if err != nil { + t.Fatal(err) + } + if err = os.MkdirAll(filepath.Dir(path), 0700); err != nil { + t.Fatal(err) + } + content, err := os.ReadFile(fixturePath) + if err != nil { + t.Fatal(err) + } + if err = os.WriteFile(path, content, 0600); err != nil { + t.Fatal(err) + } + digest, size, err := fileSHA256(path) + if err != nil { + t.Fatal(err) + } + snapshot, err := json.Marshal(request.RepositoryUploadSnapshot{UploadID: uploadID, + FileName: filepath.Base(fixturePath), Kind: request.RepositoryUploadKindRPM, + RelativePath: relative, SizeBytes: size, SHA256: digest}) + if err != nil { + t.Fatal(err) + } + return &repositorynodepb.RepositoryTaskSpec{TaskId: uuid.NewString(), RepositoryId: repositoryID, + StorageNodeId: storageNodeID, TaskType: repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_UPLOAD, + FencingToken: 1, PayloadJson: string(snapshot), PublishKey: publishKey, + Architecture: request.RepositoryArchitectureX8664, GpgPolicy: request.RepositoryGPGVerifyPackage, + ActiveRevisionId: activeRevisionID, GpgKeys: keys} +} + +func remoteGPGUploadID(t *testing.T, spec *repositorynodepb.RepositoryTaskSpec) string { + t.Helper() + var snapshot request.RepositoryUploadSnapshot + if err := json.Unmarshal([]byte(spec.GetPayloadJson()), &snapshot); err != nil { + t.Fatal(err) + } + return snapshot.UploadID +} + +func remoteGPGKeyIdentity(t *testing.T, content []byte) (string, string) { + t.Helper() + block, err := armor.Decode(bytes.NewReader(content)) + if err != nil || block.Type != openpgp.PublicKeyType { + t.Fatalf("decode repository GPG public key: %v", err) + } + entities, err := openpgp.ReadKeyRing(block.Body) + if err != nil || len(entities) != 1 || entities[0] == nil || entities[0].PrimaryKey == nil { + t.Fatalf("parse repository GPG public key: %v", err) + } + key := entities[0].PrimaryKey + return strings.ToUpper(hex.EncodeToString(key.Fingerprint[:])), strings.ToUpper(key.KeyIdString()) +} diff --git a/manager/backend/services/footstone/repositoryworker/remote_migration_executor.go b/manager/backend/services/footstone/repositoryworker/remote_migration_executor.go new file mode 100644 index 0000000000000000000000000000000000000000..114bd4412cf7997ee4b3574819bff7ce22b8c4ef --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/remote_migration_executor.go @@ -0,0 +1,710 @@ +package repositoryworker + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "sort" + "strings" + "time" + "unicode" + + "github.com/google/uuid" + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/armor" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + repositorynodepb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repositorynode" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/taskresult" +) + +const maxMigratedPublicKeyBytes = 1 << 20 + +type RemoteMigrationExecutor struct { + store *storage.Store + client *http.Client + now func() time.Time +} + +func NewRemoteMigrationExecutor(store *storage.Store) (*RemoteMigrationExecutor, error) { + if store == nil { + return nil, fmt.Errorf("remote migration executor requires repository storage") + } + transport := &http.Transport{Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{Timeout: 15 * time.Second, KeepAlive: 30 * time.Second}).DialContext, + ForceAttemptHTTP2: true, ResponseHeaderTimeout: 30 * time.Second, IdleConnTimeout: 90 * time.Second} + return &RemoteMigrationExecutor{store: store, client: &http.Client{Transport: transport, + CheckRedirect: func(_ *http.Request, _ []*http.Request) error { + return errors.New("repository migration redirects are forbidden") + }}, now: func() time.Time { return time.Now().UTC() }}, nil +} + +func (e *RemoteMigrationExecutor) Execute(ctx context.Context, + spec *repositorynodepb.RepositoryTaskSpec, +) (RemoteOutcome, error) { + if spec == nil || spec.GetTaskType() != repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_MIGRATE_REPOSITORY { + return RemoteOutcome{}, FailedExecution("UNSUPPORTED_TASK_TYPE", + "remote migration executor only accepts MIGRATE_REPOSITORY tasks") + } + if uuid.Validate(spec.GetTaskId()) != nil || uuid.Validate(spec.GetRepositoryId()) != nil || + uuid.Validate(spec.GetStorageNodeId()) != nil || uuid.Validate(spec.GetRevisionId()) != nil || + strings.TrimSpace(spec.GetPublishKey()) == "" { + return RemoteOutcome{}, FailedExecution("INVALID_TASK_SPEC", "remote migration task identity is invalid") + } + snapshot, err := decodeMigrationSnapshot(spec) + if err != nil { + return RemoteOutcome{}, FailedExecution("INVALID_MIGRATION_PAYLOAD", err.Error()) + } + if strings.TrimRight(snapshot.TargetPublishBaseURL, "/") != e.store.Config().PublicBaseURL { + return RemoteOutcome{}, FailedExecution("INVALID_MIGRATION_PAYLOAD", + "migration target publish URL does not match the repository node") + } + manifestURL, err := appendMigrationURL(snapshot.SourceRevisionURL, "manifest.json") + if err != nil { + return RemoteOutcome{}, FailedExecution("INVALID_MIGRATION_PAYLOAD", err.Error()) + } + manifestBytes, err := e.fetchBytes(ctx, manifestURL, maxPublishedManifestBytes, + snapshot.SnapshotManifestSHA256) + if err != nil { + return RemoteOutcome{}, FailedExecution("MIGRATION_MANIFEST_DOWNLOAD_FAILED", err.Error()) + } + manifest, packages, err := validateMigrationManifest(spec, snapshot, manifestBytes) + if err != nil { + return RemoteOutcome{}, FailedExecution("MIGRATION_MANIFEST_INVALID", err.Error()) + } + if err = e.copyGPGKeys(ctx, spec, snapshot); err != nil { + return RemoteOutcome{}, FailedExecution("MIGRATION_GPG_KEY_FAILED", err.Error()) + } + finalRelative, err := e.materializeRevision(ctx, spec, snapshot, manifestBytes, manifest, packages) + if err != nil { + return RemoteOutcome{}, FailedExecution("MIGRATION_TRANSFER_FAILED", err.Error()) + } + now := e.now() + revision := &po.RepositoryRevision{ID: snapshot.RevisionID, RepositoryID: spec.GetRepositoryId(), + StorageNodeID: spec.GetStorageNodeId(), TaskID: spec.GetTaskId(), Kind: snapshot.RevisionKind, + Status: request.RepositoryRevisionStatusPublished, PackageCount: snapshot.PackageCount, + TotalBytes: snapshot.TotalBytes, ManifestSHA256: snapshot.ManifestSHA256, + RepomdSHA256: snapshot.RepomdSHA256, SnapshotManifestSHA256: snapshot.SnapshotManifestSHA256, + MetadataSigned: manifest.MetadataSigned, MetadataSigningKeyRef: manifest.MetadataSigningKeyRef, + MetadataSigningFingerprint: manifest.MetadataSigningFingerprint, + RepomdSignaturePath: metadataSignaturePath(manifest), MetadataPublicKeyURL: manifest.MetadataPublicKeyURL, + RelativePath: finalRelative, PublishedAt: &now} + for index := range packages { + packages[index].RelativePath = filepath.ToSlash(filepath.Join(finalRelative, manifest.Packages[index].Path)) + } + task := &po.RepositoryTask{ID: spec.GetTaskId(), RepositoryID: spec.GetRepositoryId(), + StorageNodeID: spec.GetStorageNodeId(), WorkerID: "remote-data-plane", + FencingToken: spec.GetFencingToken()} + resultObjectID, resultObjectSHA, err := writeRemoteResultObject(e.store, task, revision, packages, + taskresult.WriteOptions{CompletionStatus: request.RepositoryTaskStatusSuccess, + AcceptedCount: len(packages)}) + if err != nil { + return RemoteOutcome{}, FailedExecution("TASK_RESULT_WRITE_FAILED", err.Error()) + } + return RemoteOutcome{RevisionID: revision.ID, ManifestSHA256: revision.SnapshotManifestSHA256, + ResultObjectID: resultObjectID, ResultObjectSHA256: resultObjectSHA, + PackageCount: int64(revision.PackageCount), TotalBytes: revision.TotalBytes, + CompletionStatus: repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_SUCCESS}, nil +} + +func (e *RemoteMigrationExecutor) Activate(_ context.Context, spec *repositorynodepb.RepositoryTaskSpec, + outcome RemoteOutcome, +) error { + if spec == nil || uuid.Validate(outcome.RevisionID) != nil || outcome.RevisionID != spec.GetRevisionId() { + return fmt.Errorf("remote migration activation identity is invalid") + } + if _, err := e.store.SwitchCurrent(spec.GetPublishKey(), outcome.RevisionID); err != nil { + return err + } + return e.store.VerifyPublishedCurrent(spec.GetPublishKey(), outcome.RevisionID) +} + +func decodeMigrationSnapshot(spec *repositorynodepb.RepositoryTaskSpec) (request.RepositoryMigrationSnapshot, error) { + var value request.RepositoryMigrationSnapshot + decoder := json.NewDecoder(strings.NewReader(spec.GetPayloadJson())) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&value); err != nil { + return value, fmt.Errorf("decode migration snapshot: %w", err) + } + if decoder.Decode(new(interface{})) != io.EOF { + return value, errors.New("migration snapshot contains trailing JSON data") + } + if uuid.Validate(value.TargetStorageNodeID) != nil || value.TargetStorageNodeID != spec.GetStorageNodeId() || + uuid.Validate(value.RevisionID) != nil || value.RevisionID != spec.GetRevisionId() || + (value.SourceStorageNodeID != "" && uuid.Validate(value.SourceStorageNodeID) != nil) || + value.SourceStorageNodeID == value.TargetStorageNodeID { + return value, errors.New("migration source, target, or revision identity is invalid") + } + if value.RevisionKind != request.RepositoryRevisionKindSync && + value.RevisionKind != request.RepositoryRevisionKindUpload && + value.RevisionKind != request.RepositoryRevisionKindDeletePackage { + return value, errors.New("migration revision kind is invalid") + } + if !validLowerDigest(value.ManifestSHA256) || !validLowerDigest(value.RepomdSHA256) || + !validLowerDigest(value.SnapshotManifestSHA256) || value.PackageCount < 0 || value.TotalBytes < 0 { + return value, errors.New("migration revision checksum or size metadata is invalid") + } + revisionURL, err := validateMigrationHTTPURL(value.SourceRevisionURL) + if err != nil || !strings.HasSuffix(strings.TrimRight(revisionURL.Path, "/"), + "/revisions/"+url.PathEscape(value.RevisionID)) { + return value, errors.New("migration source revision URL is invalid") + } + if _, err = validateMigrationHTTPURL(value.TargetPublishBaseURL); err != nil { + return value, errors.New("migration target publish URL is invalid") + } + if len(value.GPGKeys) > 64 { + return value, errors.New("migration GPG key count exceeds 64") + } + if err = validateMigrationGPGReferences(spec, value, revisionURL); err != nil { + return value, err + } + return value, nil +} + +func validateMigrationGPGReferences(spec *repositorynodepb.RepositoryTaskSpec, + snapshot request.RepositoryMigrationSnapshot, revisionURL *url.URL, +) error { + if request.RepositoryGPGPolicyEnforced(spec.GetGpgPolicy()) && len(snapshot.GPGKeys) == 0 { + return errors.New("VERIFY_PACKAGE migration requires authorized public keys") + } + if len(spec.GetGpgKeys()) != len(snapshot.GPGKeys) { + return errors.New("migration GPG task references differ from the frozen snapshot") + } + seen := make(map[string]struct{}, len(snapshot.GPGKeys)) + taskKeys := make(map[string]*repositorynodepb.RepositoryGPGKeyReference, len(spec.GetGpgKeys())) + for _, key := range spec.GetGpgKeys() { + if key == nil || taskKeys[key.GetFingerprint()] != nil { + return errors.New("migration GPG task references are nil or duplicated") + } + taskKeys[key.GetFingerprint()] = key + } + for index, key := range snapshot.GPGKeys { + key.KeyID = strings.ToUpper(strings.TrimSpace(key.KeyID)) + key.Fingerprint = strings.ToUpper(strings.TrimSpace(key.Fingerprint)) + if !authorizedKeyIDPattern.MatchString(key.KeyID) || + (len(key.Fingerprint) != 40 && len(key.Fingerprint) != 64) { + return fmt.Errorf("migration GPG key %d has invalid identity", index) + } + if _, err := hex.DecodeString(key.Fingerprint); err != nil { + return fmt.Errorf("migration GPG key %d fingerprint is invalid", index) + } + if _, duplicate := seen[key.Fingerprint]; duplicate { + return fmt.Errorf("migration GPG key %s is duplicated", key.Fingerprint) + } + seen[key.Fingerprint] = struct{}{} + expectedPath, err := migrationGPGKeyPath(spec.GetRepositoryId(), key.Fingerprint) + if err != nil || key.PublicKeyPath != expectedPath { + return fmt.Errorf("migration GPG key %s storage path is invalid", key.Fingerprint) + } + keyURL, err := validateMigrationHTTPURL(key.SourcePublicURL) + revisionSuffix := "/revisions/" + url.PathEscape(snapshot.RevisionID) + sourcePrefix := strings.TrimSuffix(strings.TrimRight(revisionURL.Path, "/"), revisionSuffix) + expectedKeyPath := sourcePrefix + "/keys/" + url.PathEscape(key.Fingerprint) + "/public.key" + if err != nil || keyURL.Scheme != revisionURL.Scheme || keyURL.Host != revisionURL.Host || + keyURL.Path != expectedKeyPath { + return fmt.Errorf("migration GPG key %s source URL is invalid", key.Fingerprint) + } + taskKey := taskKeys[key.Fingerprint] + if taskKey == nil || taskKey.GetKeyId() != key.KeyID || taskKey.GetFingerprint() != key.Fingerprint || + taskKey.GetPublicKeyPath() != key.PublicKeyPath { + return errors.New("migration GPG task references differ from the frozen snapshot") + } + } + return nil +} + +func validateMigrationManifest(spec *repositorynodepb.RepositoryTaskSpec, + snapshot request.RepositoryMigrationSnapshot, content []byte, +) (publishedManifest, []po.RepositoryPackage, error) { + var manifest publishedManifest + decoder := json.NewDecoder(bytes.NewReader(content)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&manifest); err != nil { + return manifest, nil, fmt.Errorf("decode published manifest: %w", err) + } + if decoder.Decode(new(interface{})) != io.EOF || manifest.SchemaVersion != 3 || + manifest.RepositoryID != spec.GetRepositoryId() || manifest.RevisionID != snapshot.RevisionID || + manifest.RepomdSHA256 != snapshot.RepomdSHA256 || len(manifest.Packages) != snapshot.PackageCount { + return manifest, nil, errors.New("published manifest identity, schema, or counts do not match the task") + } + packages := make([]po.RepositoryPackage, 0, len(manifest.Packages)) + seenPaths := make(map[string]struct{}, len(manifest.Packages)+len(manifest.MetadataFiles)+len(manifest.PublicFiles)+1) + seenNEVRA := make(map[string]struct{}, len(manifest.Packages)) + for _, item := range manifest.Packages { + if err := validateMigrationPackage(item); err != nil { + return manifest, nil, err + } + if _, duplicate := seenPaths[item.Path]; duplicate { + return manifest, nil, fmt.Errorf("published manifest contains duplicate path %q", item.Path) + } + if _, duplicate := seenNEVRA[item.NEVRA]; duplicate { + return manifest, nil, fmt.Errorf("published manifest contains duplicate NEVRA %q", item.NEVRA) + } + seenPaths[item.Path], seenNEVRA[item.NEVRA] = struct{}{}, struct{}{} + packages = append(packages, po.RepositoryPackage{ID: uuid.NewString(), RepositoryID: spec.GetRepositoryId(), + RevisionID: snapshot.RevisionID, Name: item.Name, Epoch: item.Epoch, Version: item.Version, + Release: item.Release, Architecture: item.Architecture, NEVRA: item.NEVRA, + FileName: item.FileName, SizeBytes: item.SizeBytes, SHA256: item.SHA256, + Source: item.Source, SourceUploadID: item.SourceUploadID, + SignatureStatus: normalizeManifestSignatureStatus(item.SignatureStatus), + SignerKeyID: item.SignerKeyID, SignatureMessage: item.SignatureMessage}) + } + repomdFound := false + for _, item := range manifest.MetadataFiles { + if err := validateMigrationMetadata(item); err != nil { + return manifest, nil, err + } + if _, duplicate := seenPaths[item.Path]; duplicate { + return manifest, nil, fmt.Errorf("published manifest contains duplicate path %q", item.Path) + } + seenPaths[item.Path] = struct{}{} + if item.Path == "repodata/repomd.xml" { + repomdFound = item.SHA256 == snapshot.RepomdSHA256 + } + } + publicKeyFound := false + for _, item := range manifest.PublicFiles { + if err := validateMigrationPublicFile(item); err != nil { + return manifest, nil, err + } + if _, duplicate := seenPaths[item.Path]; duplicate { + return manifest, nil, fmt.Errorf("published manifest contains duplicate path %q", item.Path) + } + seenPaths[item.Path] = struct{}{} + if manifest.MetadataSigned && strings.HasSuffix(manifest.MetadataPublicKeyURL, "/"+path.Base(item.Path)) { + publicKeyFound = true + } + } + if manifest.MetadataSigned { + if !metadataSigningKeyRefPattern.MatchString(manifest.MetadataSigningKeyRef) || + len(manifest.MetadataSigningFingerprint) < 40 || len(manifest.MetadataSigningFingerprint) > 64 || + !publicKeyFound || !manifestContainsPath(manifest.MetadataFiles, "repodata/repomd.xml.asc") { + return manifest, nil, errors.New("published metadata signing inventory is incomplete") + } + } + manifestDigest, totalBytes := packageManifest(packages) + if manifestDigest != snapshot.ManifestSHA256 || totalBytes != snapshot.TotalBytes || !repomdFound { + return manifest, nil, errors.New("published package or repomd checksums do not match the frozen revision") + } + return manifest, packages, nil +} + +func validateMigrationPackage(item manifestPackage) error { + if filepath.Base(item.Path) != item.Path || filepath.Base(item.FileName) != item.FileName || + !strings.HasSuffix(strings.ToLower(item.Path), ".rpm") || item.SizeBytes <= 0 || + !validLowerDigest(item.SHA256) || strings.IndexFunc(item.Path+item.FileName, unicode.IsControl) >= 0 { + return fmt.Errorf("published package %q has unsafe file metadata", item.NEVRA) + } + if item.Name == "" || item.Version == "" || item.Release == "" || item.Architecture == "" || item.NEVRA == "" || + strings.ContainsAny(item.Name+item.Version+item.Release+item.Architecture+item.NEVRA, "\r\n\t/") { + return fmt.Errorf("published package %q has invalid RPM metadata", item.NEVRA) + } + if item.Source != request.RepositoryPackageSourceSync && item.Source != request.RepositoryPackageSourceUpload { + return fmt.Errorf("published package %q has invalid source", item.NEVRA) + } + if item.Source == request.RepositoryPackageSourceUpload && uuid.Validate(item.SourceUploadID) != nil { + return fmt.Errorf("published package %q has invalid upload identity", item.NEVRA) + } + if item.Source == request.RepositoryPackageSourceSync && item.SourceUploadID != "" { + return fmt.Errorf("published package %q contains an unexpected upload identity", item.NEVRA) + } + status := normalizeManifestSignatureStatus(item.SignatureStatus) + if status != request.RepositoryPackageSignatureUnchecked && status != request.RepositoryPackageSignatureValid && + status != request.RepositoryPackageSignatureUnsigned && + status != request.RepositoryPackageSignatureUnauthorized && status != request.RepositoryPackageSignatureBad { + return fmt.Errorf("published package %q has invalid signature status", item.NEVRA) + } + if len(item.SignerKeyID) > 64 || len(item.SignatureMessage) > 1024 || + strings.IndexFunc(item.SignerKeyID+item.SignatureMessage, unicode.IsControl) >= 0 { + return fmt.Errorf("published package %q has invalid signature detail", item.NEVRA) + } + return nil +} + +func normalizeManifestSignatureStatus(value string) string { + if value == "" { + return request.RepositoryPackageSignatureUnchecked + } + return value +} + +func validateMigrationMetadata(item manifestFile) error { + clean := path.Clean(item.Path) + if clean != item.Path || !strings.HasPrefix(clean, "repodata/") || clean == "repodata/" || + strings.Contains(item.Path, "\\") || strings.IndexFunc(item.Path, unicode.IsControl) >= 0 || + item.SizeBytes <= 0 || !validLowerDigest(item.SHA256) { + return fmt.Errorf("published metadata path %q is invalid", item.Path) + } + return nil +} + +func validateMigrationPublicFile(item manifestFile) error { + clean := path.Clean(item.Path) + if clean != item.Path || !strings.HasPrefix(clean, "keys/") || clean == "keys/" || + path.Dir(clean) != "keys" || item.SizeBytes <= 0 || item.SizeBytes > maxMigratedPublicKeyBytes || + !validLowerDigest(item.SHA256) { + return fmt.Errorf("published public file %q has invalid metadata", item.Path) + } + return nil +} + +func manifestContainsPath(values []manifestFile, wanted string) bool { + for _, value := range values { + if value.Path == wanted { + return true + } + } + return false +} + +func metadataSignaturePath(manifest publishedManifest) string { + if manifest.MetadataSigned { + return "repodata/repomd.xml.asc" + } + return "" +} + +func (e *RemoteMigrationExecutor) materializeRevision(ctx context.Context, + spec *repositorynodepb.RepositoryTaskSpec, snapshot request.RepositoryMigrationSnapshot, + manifestBytes []byte, manifest publishedManifest, packages []po.RepositoryPackage, +) (string, error) { + finalRelative, err := e.store.PublicRevisionRelativePath(spec.GetPublishKey(), snapshot.RevisionID) + if err != nil { + return "", err + } + finalPath, err := e.store.Resolve(finalRelative) + if err != nil { + return "", err + } + if _, statErr := os.Lstat(finalPath); statErr == nil { + if err = verifyMigratedTree(finalPath, manifestBytes, manifest); err != nil { + return "", fmt.Errorf("existing immutable revision is inconsistent: %w", err) + } + return finalRelative, nil + } else if !os.IsNotExist(statErr) { + return "", statErr + } + var metadataBytes int64 + for _, item := range manifest.MetadataFiles { + if item.SizeBytes > e.store.Config().MaxSyncBytes-metadataBytes { + return "", errors.New("migration metadata exceeds configured transfer bounds") + } + metadataBytes += item.SizeBytes + } + for _, item := range manifest.PublicFiles { + if item.SizeBytes > e.store.Config().MaxSyncBytes-metadataBytes { + return "", errors.New("migration public metadata exceeds configured transfer bounds") + } + metadataBytes += item.SizeBytes + } + if snapshot.TotalBytes > e.store.Config().MaxSyncBytes-metadataBytes || + len(packages) > e.store.Config().MaxSyncPackages { + return "", errors.New("migration snapshot exceeds configured package or byte limits") + } + if err = e.store.EnsureFreeSpace(snapshot.TotalBytes + metadataBytes + + int64(len(manifestBytes)) + e.store.Config().MetadataReserve); err != nil { + return "", err + } + stagingRelative := filepath.ToSlash(filepath.Join("staging", "migration", spec.GetTaskId(), snapshot.RevisionID)) + stagingPath, err := e.store.Resolve(stagingRelative) + if err != nil { + return "", err + } + taskStaging, _ := e.store.Resolve(filepath.ToSlash(filepath.Join("staging", "migration", spec.GetTaskId()))) + if err = os.RemoveAll(taskStaging); err != nil { + return "", err + } + defer os.RemoveAll(taskStaging) + if err = os.MkdirAll(stagingPath, 0750); err != nil { + return "", err + } + for _, item := range manifest.PublicFiles { + if err = e.downloadExact(ctx, snapshot.SourceRevisionURL, item.Path, + filepath.Join(stagingPath, filepath.FromSlash(item.Path)), item.SizeBytes, item.SHA256); err != nil { + return "", err + } + } + for _, item := range manifest.Packages { + if err = e.downloadExact(ctx, snapshot.SourceRevisionURL, item.Path, + filepath.Join(stagingPath, filepath.FromSlash(item.Path)), item.SizeBytes, item.SHA256); err != nil { + return "", err + } + } + for _, item := range manifest.MetadataFiles { + if err = e.downloadExact(ctx, snapshot.SourceRevisionURL, item.Path, + filepath.Join(stagingPath, filepath.FromSlash(item.Path)), item.SizeBytes, item.SHA256); err != nil { + return "", err + } + } + if err = os.WriteFile(filepath.Join(stagingPath, "manifest.json"), manifestBytes, 0444); err != nil { + return "", err + } + if err = verifyMigratedTree(stagingPath, manifestBytes, manifest); err != nil { + return "", err + } + if err = os.MkdirAll(filepath.Dir(finalPath), 0755); err != nil { + return "", err + } + if err = os.Rename(stagingPath, finalPath); err != nil { + return "", fmt.Errorf("finalize migrated revision: %w", err) + } + if err = makePublishedTreeReadOnly(finalPath); err != nil { + (&Handler{store: e.store}).movePublishedToTrash(finalRelative, snapshot.RevisionID) + return "", err + } + return finalRelative, nil +} + +func (e *RemoteMigrationExecutor) copyGPGKeys(ctx context.Context, spec *repositorynodepb.RepositoryTaskSpec, + snapshot request.RepositoryMigrationSnapshot, +) error { + references := make([]signatureKeyReference, 0, len(snapshot.GPGKeys)) + for _, key := range snapshot.GPGKeys { + content, err := e.fetchBytes(ctx, key.SourcePublicURL, maxMigratedPublicKeyBytes, "") + if err != nil { + return fmt.Errorf("download GPG key %s: %w", key.Fingerprint, err) + } + fingerprint, keyID, err := migratedPublicKeyIdentity(content) + if err != nil || fingerprint != key.Fingerprint || keyID != key.KeyID { + return fmt.Errorf("GPG key %s content identity does not match the migration snapshot", key.Fingerprint) + } + relative, err := e.store.PutGPGPublicKey(spec.GetRepositoryId(), spec.GetPublishKey(), + key.Fingerprint, content) + if err != nil || relative != key.PublicKeyPath { + return fmt.Errorf("store GPG key %s: %w", key.Fingerprint, err) + } + references = append(references, signatureKeyReference{KeyID: key.KeyID, + Fingerprint: key.Fingerprint, PublicKeyPath: key.PublicKeyPath}) + } + if request.RepositoryGPGPolicyEnforced(spec.GetGpgPolicy()) { + session, err := newSignatureSessionFromReferences(ctx, e.store, spec.GetTaskId(), + spec.GetRepositoryId(), spec.GetPublishKey(), references) + if session != nil { + session.Close() + } + return err + } + return nil +} + +func (e *RemoteMigrationExecutor) fetchBytes(ctx context.Context, source string, limit int64, + expectedSHA string, +) ([]byte, error) { + value, err := validateMigrationHTTPURL(source) + if err != nil { + return nil, err + } + requestValue, err := http.NewRequestWithContext(ctx, http.MethodGet, value.String(), nil) + if err != nil { + return nil, err + } + response, err := e.client.Do(requestValue) + if err != nil { + return nil, err + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK { + return nil, fmt.Errorf("GET %s returned HTTP %d", value.Redacted(), response.StatusCode) + } + if response.ContentLength > limit { + return nil, fmt.Errorf("GET %s exceeds the %d-byte limit", value.Redacted(), limit) + } + content, err := io.ReadAll(io.LimitReader(response.Body, limit+1)) + if err != nil || int64(len(content)) == 0 || int64(len(content)) > limit { + return nil, fmt.Errorf("read bounded response: size=%d error=%v", len(content), err) + } + if expectedSHA != "" { + digest := sha256.Sum256(content) + if hex.EncodeToString(digest[:]) != expectedSHA { + return nil, errors.New("downloaded object SHA-256 does not match the frozen snapshot") + } + } + return content, nil +} + +func (e *RemoteMigrationExecutor) downloadExact(ctx context.Context, baseURL, relative, destination string, + expectedSize int64, expectedSHA string, +) error { + source, err := appendMigrationURL(baseURL, relative) + if err != nil { + return err + } + requestValue, err := http.NewRequestWithContext(ctx, http.MethodGet, source, nil) + if err != nil { + return err + } + response, err := e.client.Do(requestValue) + if err != nil { + return err + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK || + (response.ContentLength >= 0 && response.ContentLength != expectedSize) { + return fmt.Errorf("GET %s returned status=%d length=%d, expected length=%d", + source, response.StatusCode, response.ContentLength, expectedSize) + } + if err = os.MkdirAll(filepath.Dir(destination), 0750); err != nil { + return err + } + file, err := os.OpenFile(destination, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0440) + if err != nil { + return err + } + hash := sha256.New() + written, copyErr := io.Copy(io.MultiWriter(file, hash), io.LimitReader(response.Body, expectedSize+1)) + if syncErr := file.Sync(); copyErr == nil { + copyErr = syncErr + } + if closeErr := file.Close(); copyErr == nil { + copyErr = closeErr + } + if copyErr != nil || written != expectedSize || hex.EncodeToString(hash.Sum(nil)) != expectedSHA { + _ = os.Remove(destination) + return fmt.Errorf("downloaded object does not match manifest: path=%s size=%d error=%v", relative, written, copyErr) + } + return nil +} + +func verifyMigratedTree(root string, manifestBytes []byte, manifest publishedManifest) error { + expected := map[string]manifestFile{"manifest.json": {Path: "manifest.json", + SHA256: digestBytes(manifestBytes), SizeBytes: int64(len(manifestBytes))}} + for _, item := range manifest.Packages { + expected[item.Path] = manifestFile{Path: item.Path, SHA256: item.SHA256, SizeBytes: item.SizeBytes} + } + for _, item := range manifest.MetadataFiles { + expected[item.Path] = item + } + for _, item := range manifest.PublicFiles { + expected[item.Path] = item + } + seen := make(map[string]struct{}, len(expected)) + err := filepath.Walk(root, func(value string, info os.FileInfo, walkErr error) error { + if walkErr != nil { + return walkErr + } + if info.Mode()&os.ModeSymlink != 0 { + return errors.New("migrated revision contains a symlink") + } + if info.IsDir() { + return nil + } + if !info.Mode().IsRegular() { + return errors.New("migrated revision contains a non-regular file") + } + relative, relativeErr := filepath.Rel(root, value) + if relativeErr != nil { + return relativeErr + } + relative = filepath.ToSlash(relative) + record, ok := expected[relative] + if !ok { + return fmt.Errorf("migrated revision contains unexpected file %q", relative) + } + digest, size, hashErr := fileSHA256(value) + if hashErr != nil || digest != record.SHA256 || size != record.SizeBytes { + return fmt.Errorf("migrated file %q does not match its manifest", relative) + } + seen[relative] = struct{}{} + return nil + }) + if err != nil { + return err + } + if len(seen) != len(expected) { + missing := make([]string, 0) + for value := range expected { + if _, ok := seen[value]; !ok { + missing = append(missing, value) + } + } + sort.Strings(missing) + return fmt.Errorf("migrated revision is missing files: %s", strings.Join(missing, ", ")) + } + return nil +} + +func migratedPublicKeyIdentity(content []byte) (string, string, error) { + if len(content) == 0 || len(content) > maxMigratedPublicKeyBytes || + bytes.Contains(content, []byte("-----BEGIN PGP PRIVATE KEY BLOCK-----")) { + return "", "", errors.New("migration public key content is invalid") + } + block, err := armor.Decode(bytes.NewReader(content)) + if err != nil || block.Type != openpgp.PublicKeyType { + return "", "", errors.New("migration public key must be an ASCII-armored OpenPGP public key") + } + entities, err := openpgp.ReadKeyRing(block.Body) + if err != nil || len(entities) != 1 || entities[0] == nil || entities[0].PrimaryKey == nil || + entities[0].PrivateKey != nil { + return "", "", errors.New("migration public key must contain exactly one public identity") + } + for _, subkey := range entities[0].Subkeys { + if subkey.PrivateKey != nil { + return "", "", errors.New("migration public key contains private subkey material") + } + } + fingerprint := strings.ToUpper(hex.EncodeToString(entities[0].PrimaryKey.Fingerprint[:])) + return fingerprint, strings.ToUpper(entities[0].PrimaryKey.KeyIdString()), nil +} + +func migrationGPGKeyPath(repositoryID, fingerprint string) (string, error) { + if uuid.Validate(repositoryID) != nil || (len(fingerprint) != 40 && len(fingerprint) != 64) { + return "", errors.New("migration GPG key identity is invalid") + } + return filepath.ToSlash(filepath.Join("keys", repositoryID, fingerprint, "public.key")), nil +} + +func appendMigrationURL(baseURL, relative string) (string, error) { + base, err := validateMigrationHTTPURL(baseURL) + if err != nil { + return "", err + } + clean := path.Clean(relative) + if clean != relative || clean == "." || strings.HasPrefix(clean, "../") || strings.Contains(relative, "\\") { + return "", errors.New("migration object path is invalid") + } + components := strings.Split(clean, "/") + for index := range components { + components[index] = url.PathEscape(components[index]) + } + base.Path = strings.TrimRight(base.Path, "/") + "/" + strings.Join(components, "/") + base.RawPath = "" + return base.String(), nil +} + +func validateMigrationHTTPURL(value string) (*url.URL, error) { + parsed, err := url.Parse(strings.TrimSpace(value)) + if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" || + parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" || parsed.Opaque != "" { + return nil, errors.New("migration URL must be absolute credential-free HTTP(S)") + } + return parsed, nil +} + +func validLowerDigest(value string) bool { + if len(value) != 64 || strings.ToLower(value) != value { + return false + } + _, err := hex.DecodeString(value) + return err == nil +} + +func digestBytes(value []byte) string { + digest := sha256.Sum256(value) + return hex.EncodeToString(digest[:]) +} + +var _ RemoteExecutor = (*RemoteMigrationExecutor)(nil) diff --git a/manager/backend/services/footstone/repositoryworker/remote_migration_executor_test.go b/manager/backend/services/footstone/repositoryworker/remote_migration_executor_test.go new file mode 100644 index 0000000000000000000000000000000000000000..067303d3614d9c741d75cf6f2d0d8a2925bb56b6 --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/remote_migration_executor_test.go @@ -0,0 +1,199 @@ +package repositoryworker + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + repositorynodepb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repositorynode" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/taskresult" +) + +const ( + remoteMigrationRevisionID = "44444444-4444-4444-8444-444444444444" + remoteMigrationSourceID = "55555555-5555-4555-8555-555555555555" +) + +func TestRemoteMigrationExecutorCopiesImmutableRevisionBeforeActivation(t *testing.T) { + store, executor, spec, server, _ := remoteMigrationFixture(t, nil) + defer server.Close() + outcome, err := executor.Execute(context.Background(), spec) + if err != nil { + t.Fatal(err) + } + current := filepath.Join(store.Root(), "public", spec.GetPublishKey(), "current") + if _, err = os.Lstat(current); !os.IsNotExist(err) { + t.Fatalf("current link exists before migration activation: %v", err) + } + resultPath, err := store.Resolve(outcome.ResultObjectID) + if err != nil { + t.Fatal(err) + } + result, err := os.Open(resultPath) + if err != nil { + t.Fatal(err) + } + document, parseErr := taskresult.ParseZstd(result, taskresult.Expectation{ + TaskID: spec.GetTaskId(), RepositoryID: spec.GetRepositoryId(), StorageNodeID: spec.GetStorageNodeId(), + RevisionID: remoteMigrationRevisionID, PublishKey: spec.GetPublishKey(), + ManifestSHA256: outcome.ManifestSHA256, Kind: request.RepositoryRevisionKindSync, + Architecture: spec.GetArchitecture(), PackageCount: outcome.PackageCount, TotalBytes: outcome.TotalBytes, + CompletionStatus: request.RepositoryTaskStatusSuccess, + }) + _ = result.Close() + if parseErr != nil { + t.Fatal(parseErr) + } + if len(document.Packages) != 1 || document.Packages[0].NEVRA != "ocm-sync-only-0:1.0-1.x86_64" { + t.Fatalf("migration result packages = %#v", document.Packages) + } + if err = executor.Activate(context.Background(), spec, outcome); err != nil { + t.Fatal(err) + } + target, err := os.Readlink(current) + if err != nil || filepath.ToSlash(target) != "revisions/"+remoteMigrationRevisionID { + t.Fatalf("current target/error = %q/%v", target, err) + } +} + +func TestRemoteMigrationExecutorChecksumFailureKeepsActiveRevision(t *testing.T) { + store, executor, spec, server, packagePath := remoteMigrationFixture(t, func(_ *publishedManifest) {}) + defer server.Close() + previous := "66666666-6666-4666-8666-666666666666" + previousRepomd := filepath.Join(store.Root(), "public", spec.GetPublishKey(), "revisions", previous, + "repodata", "repomd.xml") + if err := os.MkdirAll(filepath.Dir(previousRepomd), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(previousRepomd, []byte("previous"), 0444); err != nil { + t.Fatal(err) + } + if _, err := store.SwitchCurrent(spec.GetPublishKey(), previous); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(packagePath, []byte("tampered package"), 0644); err != nil { + t.Fatal(err) + } + _, err := executor.Execute(context.Background(), spec) + var failure *ExecutionError + if !errors.As(err, &failure) || failure.Code != "MIGRATION_TRANSFER_FAILED" { + t.Fatalf("Execute() checksum error = %v", err) + } + current := filepath.Join(store.Root(), "public", spec.GetPublishKey(), "current") + target, readErr := os.Readlink(current) + if readErr != nil || filepath.ToSlash(target) != "revisions/"+previous { + t.Fatalf("active revision changed after failed migration: target=%q error=%v", target, readErr) + } +} + +func TestRemoteMigrationExecutorRejectsUnsafeManifestPath(t *testing.T) { + store, executor, spec, server, _ := remoteMigrationFixture(t, func(manifest *publishedManifest) { + manifest.Packages[0].Path = "../escaped.rpm" + }) + defer server.Close() + _, err := executor.Execute(context.Background(), spec) + var failure *ExecutionError + if !errors.As(err, &failure) || failure.Code != "MIGRATION_MANIFEST_INVALID" { + t.Fatalf("Execute() unsafe path error = %v", err) + } + if _, statErr := os.Stat(filepath.Join(store.Root(), "escaped.rpm")); !os.IsNotExist(statErr) { + t.Fatalf("unsafe migration path escaped repository root: %v", statErr) + } +} + +func remoteMigrationFixture(t *testing.T, mutate func(*publishedManifest)) ( + *storage.Store, *RemoteMigrationExecutor, *repositorynodepb.RepositoryTaskSpec, *httptest.Server, string, +) { + t.Helper() + targetRoot := t.TempDir() + t.Cleanup(func() { makeRemoteTestTreeWritable(targetRoot) }) + store, err := storage.New(config.RepositoryConfig{Root: targetRoot, + PublicBaseURL: "http://target.example/repositories", MaxSyncBytes: 1 << 20, + MaxSyncPackages: 10, MetadataReserve: 1, MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + sourceRoot := t.TempDir() + sourceRevision := filepath.Join(sourceRoot, "published", "oc9-baseos", "revisions", remoteMigrationRevisionID) + packageName := "fixture.rpm" + packagePath := filepath.Join(sourceRevision, packageName) + repomdPath := filepath.Join(sourceRevision, "repodata", "repomd.xml") + if err = os.MkdirAll(filepath.Dir(repomdPath), 0755); err != nil { + t.Fatal(err) + } + if err = os.WriteFile(packagePath, []byte("immutable rpm fixture"), 0644); err != nil { + t.Fatal(err) + } + if err = os.WriteFile(repomdPath, []byte(""), 0644); err != nil { + t.Fatal(err) + } + packageSHA, packageSize, err := fileSHA256(packagePath) + if err != nil { + t.Fatal(err) + } + repomdSHA, repomdSize, err := fileSHA256(repomdPath) + if err != nil { + t.Fatal(err) + } + packageValue := po.RepositoryPackage{RepositoryID: remoteTestRepoID, + RevisionID: remoteMigrationRevisionID, Name: "ocm-sync-only", Epoch: "0", Version: "1.0", + Release: "1", Architecture: request.RepositoryArchitectureX8664, + NEVRA: "ocm-sync-only-0:1.0-1.x86_64", FileName: packageName, + SizeBytes: packageSize, SHA256: packageSHA, Source: request.RepositoryPackageSourceSync} + manifestSHA, totalBytes := packageManifest([]po.RepositoryPackage{packageValue}) + manifest := publishedManifest{SchemaVersion: 3, RepositoryID: remoteTestRepoID, + RevisionID: remoteMigrationRevisionID, RepomdSHA256: repomdSHA, + Packages: []manifestPackage{{Name: packageValue.Name, Epoch: packageValue.Epoch, + Version: packageValue.Version, Release: packageValue.Release, + Architecture: packageValue.Architecture, NEVRA: packageValue.NEVRA, + FileName: packageName, Path: packageName, SHA256: packageSHA, + SizeBytes: packageSize, Source: packageValue.Source}}, + MetadataFiles: []manifestFile{{Path: "repodata/repomd.xml", SHA256: repomdSHA, + SizeBytes: repomdSize}}} + if mutate != nil { + mutate(&manifest) + } + manifestBytes, err := json.MarshalIndent(manifest, "", " ") + if err != nil { + t.Fatal(err) + } + manifestBytes = append(manifestBytes, '\n') + if err = os.WriteFile(filepath.Join(sourceRevision, "manifest.json"), manifestBytes, 0644); err != nil { + t.Fatal(err) + } + server := httptest.NewServer(http.FileServer(http.Dir(sourceRoot))) + snapshot := request.RepositoryMigrationSnapshot{SourceStorageNodeID: remoteMigrationSourceID, + TargetStorageNodeID: remoteTestNodeID, + TargetPublishBaseURL: "http://target.example/repositories", + SourceRevisionURL: server.URL + "/published/oc9-baseos/revisions/" + remoteMigrationRevisionID, + RevisionID: remoteMigrationRevisionID, RevisionKind: request.RepositoryRevisionKindSync, + ManifestSHA256: manifestSHA, RepomdSHA256: repomdSHA, + SnapshotManifestSHA256: digestBytes(manifestBytes), PackageCount: 1, TotalBytes: totalBytes} + payload, err := json.Marshal(snapshot) + if err != nil { + t.Fatal(err) + } + spec := &repositorynodepb.RepositoryTaskSpec{TaskId: remoteTestTaskID, RepositoryId: remoteTestRepoID, + StorageNodeId: remoteTestNodeID, RevisionId: remoteMigrationRevisionID, + TaskType: repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_MIGRATE_REPOSITORY, + FencingToken: 14, PayloadJson: string(payload), PublishKey: "oc9-baseos", + Architecture: request.RepositoryArchitectureX8664, GpgPolicy: request.RepositoryGPGDisabled, + ActiveRevisionId: remoteMigrationRevisionID} + executor, err := NewRemoteMigrationExecutor(store) + if err != nil { + t.Fatal(err) + } + executor.client = server.Client() + executor.now = testNow + return store, executor, spec, server, packagePath +} diff --git a/manager/backend/services/footstone/repositoryworker/remote_migration_gpg_integration_test.go b/manager/backend/services/footstone/repositoryworker/remote_migration_gpg_integration_test.go new file mode 100644 index 0000000000000000000000000000000000000000..b9c0b272503917fccdcc1d62bc471ead3a6aef7f --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/remote_migration_gpg_integration_test.go @@ -0,0 +1,101 @@ +//go:build integration + +package repositoryworker + +import ( + "bytes" + "context" + "encoding/json" + "os" + "os/exec" + "path/filepath" + "testing" + + repositorynodepb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repositorynode" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" +) + +func TestRemoteMigrationExecutorCopiesAuthorizedGPGPublicKey(t *testing.T) { + if _, err := exec.LookPath("rpmkeys"); err != nil { + t.Skip("rpmkeys is required for repository migration GPG integration") + } + publicKeyPath := os.Getenv("OCM_REPOSITORY_GPG_PUBLIC_KEY") + if publicKeyPath == "" { + t.Skip("OCM_REPOSITORY_GPG_PUBLIC_KEY is not configured") + } + publicKey, err := os.ReadFile(publicKeyPath) + if err != nil || len(publicKey) == 0 { + t.Fatalf("read repository migration public key fixture: size=%d error=%v", len(publicKey), err) + } + fingerprint, keyID, err := migratedPublicKeyIdentity(publicKey) + if err != nil { + t.Fatalf("parse repository migration public key fixture: %v", err) + } + + store, executor, spec, server, packagePath := remoteMigrationFixture(t, nil) + defer server.Close() + sourceRoot := filepath.Clean(filepath.Join(filepath.Dir(packagePath), "..", "..", "..", "..")) + sourceKeyPath := filepath.Join(sourceRoot, "published", spec.GetPublishKey(), "keys", fingerprint, + "public.key") + if err = os.MkdirAll(filepath.Dir(sourceKeyPath), 0755); err != nil { + t.Fatal(err) + } + if err = os.WriteFile(sourceKeyPath, publicKey, 0644); err != nil { + t.Fatal(err) + } + + privateRelative, err := store.GPGKeyRelativePath(spec.GetRepositoryId(), fingerprint) + if err != nil { + t.Fatal(err) + } + var snapshot request.RepositoryMigrationSnapshot + if err = json.Unmarshal([]byte(spec.GetPayloadJson()), &snapshot); err != nil { + t.Fatal(err) + } + snapshot.GPGKeys = []request.RepositoryMigrationGPGKey{{ + KeyID: keyID, + Fingerprint: fingerprint, + PublicKeyPath: privateRelative, + SourcePublicURL: server.URL + "/published/" + spec.GetPublishKey() + "/keys/" + fingerprint + "/public.key", + }} + payload, err := json.Marshal(snapshot) + if err != nil { + t.Fatal(err) + } + spec.PayloadJson = string(payload) + spec.GpgPolicy = request.RepositoryGPGVerifyPackage + spec.GpgKeys = []*repositorynodepb.RepositoryGPGKeyReference{{ + KeyId: keyID, Fingerprint: fingerprint, PublicKeyPath: privateRelative, + }} + + outcome, err := executor.Execute(context.Background(), spec) + if err != nil { + t.Fatal(err) + } + if err = store.VerifyGPGPublicKey(spec.GetRepositoryId(), spec.GetPublishKey(), fingerprint, + privateRelative); err != nil { + t.Fatalf("verify migrated repository GPG public key: %v", err) + } + publicRelative, err := store.PublicGPGKeyRelativePath(spec.GetPublishKey(), fingerprint) + if err != nil { + t.Fatal(err) + } + for _, relative := range []string{privateRelative, publicRelative} { + path, resolveErr := store.Resolve(relative) + if resolveErr != nil { + t.Fatal(resolveErr) + } + content, readErr := os.ReadFile(path) + if readErr != nil || !bytes.Equal(content, publicKey) { + t.Fatalf("migrated public key %s differs from source: %v", relative, readErr) + } + if bytes.Contains(content, []byte("PRIVATE KEY")) { + t.Fatalf("migrated public key %s contains private key material", relative) + } + } + if err = executor.Activate(context.Background(), spec, outcome); err != nil { + t.Fatal(err) + } + t.Logf("remote_migration_gpg=PASS repository=%s revision=%s key_id=%s fingerprint=%s", + spec.GetRepositoryId(), outcome.RevisionID, keyID, fingerprint) +} diff --git a/manager/backend/services/footstone/repositoryworker/remote_sync_executor.go b/manager/backend/services/footstone/repositoryworker/remote_sync_executor.go new file mode 100644 index 0000000000000000000000000000000000000000..3071de5934f36e9c1d0b85559d67630e47d39d2b --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/remote_sync_executor.go @@ -0,0 +1,231 @@ +package repositoryworker + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/google/uuid" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + repositorynodepb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repositorynode" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/taskresult" +) + +type RemoteSyncExecutor struct { + store *storage.Store + rpm RPMTool + metadata MetadataTool + syncer SyncTool + now func() time.Time +} + +func NewRemoteSyncExecutor(store *storage.Store) (*RemoteSyncExecutor, error) { + if store == nil { + return nil, fmt.Errorf("remote sync executor requires repository storage") + } + return &RemoteSyncExecutor{store: store, rpm: commandRPMTool{}, metadata: commandMetadataTool{}, + syncer: commandSyncTool{}, now: func() time.Time { return time.Now().UTC() }}, nil +} + +func (e *RemoteSyncExecutor) Execute(ctx context.Context, + spec *repositorynodepb.RepositoryTaskSpec, +) (RemoteOutcome, error) { + if spec == nil || spec.GetTaskType() != repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_SYNC { + return RemoteOutcome{}, FailedExecution("UNSUPPORTED_TASK_TYPE", "remote sync executor only accepts SYNC tasks") + } + if uuid.Validate(spec.GetTaskId()) != nil || uuid.Validate(spec.GetRepositoryId()) != nil || + uuid.Validate(spec.GetStorageNodeId()) != nil || strings.TrimSpace(spec.GetPublishKey()) == "" { + return RemoteOutcome{}, FailedExecution("INVALID_TASK_SPEC", "remote sync task identity is invalid") + } + var snapshot request.RepositorySyncSnapshot + if err := json.Unmarshal([]byte(spec.GetPayloadJson()), &snapshot); err != nil { + return RemoteOutcome{}, FailedExecution("INVALID_SYNC_PAYLOAD", "sync task payload is invalid") + } + normalizeSyncSnapshot(&snapshot) + repository := &po.SoftwareRepository{ID: spec.GetRepositoryId(), PublishKey: spec.GetPublishKey(), + Architecture: spec.GetArchitecture(), GPGPolicy: spec.GetGpgPolicy(), + PublishBaseURLSnapshot: spec.GetPublishBaseUrl()} + applyMetadataSigningReference(repository, spec.GetMetadataSigning()) + if err := validateSyncSnapshot(repository, snapshot); err != nil { + return RemoteOutcome{}, FailedExecution("INVALID_SYNC_PAYLOAD", err.Error()) + } + task := &po.RepositoryTask{ID: spec.GetTaskId(), RepositoryID: spec.GetRepositoryId(), + StorageNodeID: spec.GetStorageNodeId(), WorkerID: "remote-data-plane", FencingToken: spec.GetFencingToken()} + verifier, err := newRemoteSignatureSession(ctx, e.store, spec) + if err != nil { + return RemoteOutcome{}, FailedExecution("GPG_KEY_SYNC_REQUIRED", err.Error()) + } + if verifier != nil { + defer verifier.Close() + } + taskRelative := filepath.ToSlash(filepath.Join("staging", "sync", task.ID)) + taskRoot, err := e.store.Resolve(taskRelative) + if err != nil { + return RemoteOutcome{}, FailedExecution("SYNC_STAGING_FAILED", err.Error()) + } + if err = os.RemoveAll(taskRoot); err != nil { + return RemoteOutcome{}, FailedExecution("SYNC_STAGING_FAILED", err.Error()) + } + defer os.RemoveAll(taskRoot) + syncBytes, _ := syncLimits(e.store.Config(), snapshot) + if err = e.store.EnsureFreeSpace(syncBytes + e.store.Config().MetadataReserve); err != nil { + return RemoteOutcome{}, FailedExecution("SYNC_INSUFFICIENT_SPACE", err.Error()) + } + if err = os.MkdirAll(taskRoot, 0700); err != nil { + return RemoteOutcome{}, FailedExecution("SYNC_STAGING_FAILED", err.Error()) + } + unmatched, err := e.syncer.Download(ctx, snapshot, taskRoot) + if err != nil { + return RemoteOutcome{}, FailedExecution("SYNC_DOWNLOAD_FAILED", err.Error()) + } + handler := &Handler{store: e.store, rpm: e.rpm, metadata: e.metadata, syncer: e.syncer, now: e.now} + candidates, err := handler.inspectSyncedPackages(ctx, repository, snapshot, + filepath.Join(taskRoot, "packages"), verifier) + if err != nil { + return RemoteOutcome{}, FailedExecution("SYNC_VALIDATION_FAILED", err.Error()) + } + if len(candidates) == 0 { + return RemoteOutcome{}, FailedExecution("SYNC_EMPTY_RESULT", syncFilterSummary(unmatched)) + } + var parent *po.RepositoryRevision + if spec.GetActiveRevisionId() != "" { + if uuid.Validate(spec.GetActiveRevisionId()) != nil { + return RemoteOutcome{}, FailedExecution("INVALID_TASK_SPEC", "active revision id is invalid") + } + parent = &po.RepositoryRevision{ID: spec.GetActiveRevisionId()} + } + if snapshot.SyncMode == request.RepositorySyncModeAppend && parent != nil { + candidates, err = handler.mergeActiveRevision(ctx, repository, parent.ID, candidates) + if err != nil { + return RemoteOutcome{}, FailedExecution("REVISION_BUILD_FAILED", err.Error()) + } + } + revision, packages, privateRelative, err := handler.materializeSyncRevision(task, repository, parent, candidates) + if err != nil { + return RemoteOutcome{}, FailedExecution("REVISION_BUILD_FAILED", err.Error()) + } + finalRelative, published, err := handler.materializePublishedRevision(ctx, task, repository, revision, packages) + if err != nil { + handler.moveRevisionToTrash(privateRelative, revision.ID) + return RemoteOutcome{}, FailedExecution("REVISION_PUBLISH_FAILED", err.Error()) + } + resultObjectID, resultObjectSHA, err := writeRemoteResultObject(e.store, task, revision, published, + taskresult.WriteOptions{CompletionStatus: request.RepositoryTaskStatusSuccess, + AcceptedCount: len(published)}) + if err != nil { + handler.movePublishedToTrash(finalRelative, revision.ID) + handler.moveRevisionToTrash(privateRelative, revision.ID) + return RemoteOutcome{}, FailedExecution("TASK_RESULT_WRITE_FAILED", err.Error()) + } + return RemoteOutcome{RevisionID: revision.ID, ManifestSHA256: revision.SnapshotManifestSHA256, + ResultObjectID: resultObjectID, ResultObjectSHA256: resultObjectSHA, + PackageCount: int64(revision.PackageCount), TotalBytes: revision.TotalBytes, + CompletionStatus: repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_SUCCESS}, nil +} + +func (e *RemoteSyncExecutor) Activate(_ context.Context, spec *repositorynodepb.RepositoryTaskSpec, + outcome RemoteOutcome, +) error { + if spec == nil || uuid.Validate(outcome.RevisionID) != nil { + return fmt.Errorf("remote sync activation identity is invalid") + } + if _, err := e.store.SwitchCurrent(spec.GetPublishKey(), outcome.RevisionID); err != nil { + return err + } + return e.store.VerifyPublishedCurrent(spec.GetPublishKey(), outcome.RevisionID) +} + +func writeRemoteResultObject(store *storage.Store, task *po.RepositoryTask, revision *po.RepositoryRevision, + packages []po.RepositoryPackage, options taskresult.WriteOptions, +) (string, string, error) { + relative := filepath.ToSlash(filepath.Join("task-logs", task.ID, "result.jsonl.zst")) + path, err := store.Resolve(relative) + if err != nil { + return "", "", err + } + if err = os.MkdirAll(filepath.Dir(path), 0750); err != nil { + return "", "", err + } + temporary := path + ".tmp" + if err = os.Remove(temporary); err != nil && !os.IsNotExist(err) { + return "", "", err + } + file, err := os.OpenFile(temporary, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) + if err != nil { + return "", "", err + } + err = taskresult.WriteZstdWithOptions(file, task, revision, packages, options) + if syncErr := file.Sync(); err == nil { + err = syncErr + } + if closeErr := file.Close(); err == nil { + err = closeErr + } + if err != nil { + _ = os.Remove(temporary) + return "", "", err + } + if err = os.Rename(temporary, path); err != nil { + _ = os.Remove(temporary) + return "", "", err + } + if err = os.Chmod(path, 0440); err != nil { + return "", "", err + } + digest, _, err := fileSHA256(path) + if err != nil { + return "", "", err + } + return relative, digest, nil +} + +func writeRemoteFailureObject(store *storage.Store, task *po.RepositoryTask, upload *po.RepositoryUpload, + quarantine []po.RepositoryQuarantineItem, code, message string, +) (string, string, error) { + relative := filepath.ToSlash(filepath.Join("task-logs", task.ID, "result.jsonl.zst")) + path, err := store.Resolve(relative) + if err != nil { + return "", "", err + } + if err = os.MkdirAll(filepath.Dir(path), 0750); err != nil { + return "", "", err + } + temporary := path + ".tmp" + if err = os.Remove(temporary); err != nil && !os.IsNotExist(err) { + return "", "", err + } + file, err := os.OpenFile(temporary, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) + if err != nil { + return "", "", err + } + err = taskresult.WriteFailureZstd(file, task, upload, quarantine, code, message) + if syncErr := file.Sync(); err == nil { + err = syncErr + } + if closeErr := file.Close(); err == nil { + err = closeErr + } + if err != nil { + _ = os.Remove(temporary) + return "", "", err + } + if err = os.Rename(temporary, path); err != nil { + _ = os.Remove(temporary) + return "", "", err + } + if err = os.Chmod(path, 0440); err != nil { + return "", "", err + } + digest, _, err := fileSHA256(path) + return relative, digest, err +} + +var _ RemoteExecutor = (*RemoteSyncExecutor)(nil) diff --git a/manager/backend/services/footstone/repositoryworker/remote_sync_executor_test.go b/manager/backend/services/footstone/repositoryworker/remote_sync_executor_test.go new file mode 100644 index 0000000000000000000000000000000000000000..5b9f64c10cee719bad358157d32e321e2de9af90 --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/remote_sync_executor_test.go @@ -0,0 +1,138 @@ +package repositoryworker + +import ( + "context" + "encoding/json" + "errors" + "os" + "path/filepath" + "testing" + "time" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + repositorynodepb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repositorynode" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/taskresult" +) + +func TestRemoteSyncExecutorBuildsImmutableResultBeforeActivation(t *testing.T) { + root := t.TempDir() + t.Cleanup(func() { makeRemoteTestTreeWritable(root) }) + store, err := storage.New(config.RepositoryConfig{Root: root, MaxSyncBytes: 1024, + MetadataReserve: 1, MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + executor := &RemoteSyncExecutor{store: store, rpm: remoteFakeRPM{}, metadata: remoteFakeMetadata{}, + syncer: remoteFakeSync{}, now: testNow} + spec := remoteSyncSpec(request.RepositoryGPGDisabled) + outcome, err := executor.Execute(context.Background(), spec) + if err != nil { + t.Fatal(err) + } + if _, err = os.Lstat(filepath.Join(store.Root(), "public", spec.GetPublishKey(), "current")); !os.IsNotExist(err) { + t.Fatalf("current link exists before Prepare/Activate: %v", err) + } + resultPath, err := store.Resolve(outcome.ResultObjectID) + if err != nil { + t.Fatal(err) + } + file, err := os.Open(resultPath) + if err != nil { + t.Fatal(err) + } + defer file.Close() + document, err := taskresult.ParseZstd(file, taskresult.Expectation{TaskID: spec.GetTaskId(), + RepositoryID: spec.GetRepositoryId(), StorageNodeID: spec.GetStorageNodeId(), + RevisionID: outcome.RevisionID, PublishKey: spec.GetPublishKey(), + ManifestSHA256: outcome.ManifestSHA256, Kind: request.RepositoryRevisionKindSync, + Architecture: spec.GetArchitecture(), PackageCount: outcome.PackageCount, TotalBytes: outcome.TotalBytes}) + if err != nil { + t.Fatal(err) + } + if document.Revision.RevisionID != outcome.RevisionID || len(document.Packages) != 1 || + document.Packages[0].NEVRA == "" { + t.Fatalf("result document = %#v", document) + } + if err = executor.Activate(context.Background(), spec, outcome); err != nil { + t.Fatal(err) + } + link := filepath.Join(store.Root(), "public", spec.GetPublishKey(), "current") + target, err := os.Readlink(link) + if err != nil { + t.Fatal(err) + } + if filepath.ToSlash(target) != "revisions/"+outcome.RevisionID { + t.Fatalf("current target = %q", target) + } +} + +func makeRemoteTestTreeWritable(root string) { + _ = filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return nil + } + if info.IsDir() { + _ = os.Chmod(path, 0755) + } else if info.Mode()&os.ModeSymlink == 0 { + _ = os.Chmod(path, 0644) + } + return nil + }) +} + +func TestRemoteSyncExecutorFailsClosedUntilGPGKeysAreSynchronized(t *testing.T) { + store, err := storage.New(config.RepositoryConfig{Root: t.TempDir(), MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + executor := &RemoteSyncExecutor{store: store, rpm: remoteFakeRPM{}, metadata: remoteFakeMetadata{}, + syncer: remoteFakeSync{}, now: testNow} + _, err = executor.Execute(context.Background(), remoteSyncSpec(request.RepositoryGPGVerifyPackage)) + var failure *ExecutionError + if !errors.As(err, &failure) || failure.Code != "GPG_KEY_SYNC_REQUIRED" { + t.Fatalf("Execute() GPG error = %v", err) + } +} + +func remoteSyncSpec(policy string) *repositorynodepb.RepositoryTaskSpec { + payload, _ := json.Marshal(request.RepositorySyncSnapshot{UpstreamURL: "https://repo.example.invalid/oc9/", + Architecture: request.RepositoryArchitectureX8664}) + return &repositorynodepb.RepositoryTaskSpec{TaskId: remoteTestTaskID, RepositoryId: remoteTestRepoID, + StorageNodeId: remoteTestNodeID, TaskType: repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_SYNC, + FencingToken: 10, PayloadJson: string(payload), PublishKey: "oc9-baseos", + Architecture: request.RepositoryArchitectureX8664, GpgPolicy: policy} +} + +type remoteFakeSync struct{} + +func (remoteFakeSync) Download(_ context.Context, _ request.RepositorySyncSnapshot, root string) ([]string, error) { + path := filepath.Join(root, "packages", "ocm-sync-only-1.0-1.x86_64.rpm") + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + return nil, err + } + return nil, os.WriteFile(path, []byte("rpm fixture"), 0600) +} + +type remoteFakeRPM struct{} + +func (remoteFakeRPM) Inspect(context.Context, string) (RPMInfo, error) { + return RPMInfo{Name: "ocm-sync-only", Version: "1.0", Release: "1", + Architecture: request.RepositoryArchitectureX8664}, nil +} + +type remoteFakeMetadata struct{} + +func (remoteFakeMetadata) Create(_ context.Context, root, revisionID string) error { + path := filepath.Join(root, "repodata", "repomd.xml") + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return err + } + return os.WriteFile(path, []byte(""), 0644) +} + +func testNow() time.Time { + return time.Date(2026, time.August, 29, 0, 0, 0, 0, time.UTC) +} diff --git a/manager/backend/services/footstone/repositoryworker/remote_upload_executor.go b/manager/backend/services/footstone/repositoryworker/remote_upload_executor.go new file mode 100644 index 0000000000000000000000000000000000000000..55a0f6f38c519468b121c95da5701de336810268 --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/remote_upload_executor.go @@ -0,0 +1,392 @@ +package repositoryworker + +import ( + "context" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "time" + "unicode" + + "github.com/google/uuid" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + repositorynodepb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repositorynode" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/taskresult" +) + +const maxPublishedManifestBytes = 32 << 20 + +type RemoteUploadExecutor struct { + store *storage.Store + rpm RPMTool + metadata MetadataTool + now func() time.Time +} + +func NewRemoteUploadExecutor(store *storage.Store) (*RemoteUploadExecutor, error) { + if store == nil { + return nil, fmt.Errorf("remote upload executor requires repository storage") + } + return &RemoteUploadExecutor{store: store, rpm: commandRPMTool{}, metadata: commandMetadataTool{}, + now: func() time.Time { return time.Now().UTC() }}, nil +} + +func (e *RemoteUploadExecutor) Execute(ctx context.Context, + spec *repositorynodepb.RepositoryTaskSpec, +) (RemoteOutcome, error) { + if spec == nil || spec.GetTaskType() != repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_UPLOAD { + return RemoteOutcome{}, FailedExecution("UNSUPPORTED_TASK_TYPE", "remote upload executor only accepts UPLOAD tasks") + } + if uuid.Validate(spec.GetTaskId()) != nil || uuid.Validate(spec.GetRepositoryId()) != nil || + uuid.Validate(spec.GetStorageNodeId()) != nil || strings.TrimSpace(spec.GetPublishKey()) == "" { + return RemoteOutcome{}, FailedExecution("INVALID_TASK_SPEC", "remote upload task identity is invalid") + } + verifier, err := newRemoteSignatureSession(ctx, e.store, spec) + if err != nil { + return RemoteOutcome{}, FailedExecution("GPG_KEY_SYNC_REQUIRED", err.Error()) + } + if verifier != nil { + defer verifier.Close() + } + snapshot, err := decodeUploadSnapshot(spec.GetPayloadJson()) + if err != nil { + return RemoteOutcome{}, FailedExecution("INVALID_UPLOAD_PAYLOAD", err.Error()) + } + if snapshot.RelativePath != e.store.UploadRelativePath(snapshot.UploadID) { + return RemoteOutcome{}, FailedExecution("INVALID_UPLOAD_PAYLOAD", "upload object path does not match upload identity") + } + uploadPath, err := e.store.Resolve(snapshot.RelativePath) + if err != nil { + return RemoteOutcome{}, FailedExecution("UPLOAD_OBJECT_INVALID", err.Error()) + } + digest, size, err := fileSHA256(uploadPath) + if err != nil { + code := "UPLOAD_OBJECT_UNREADABLE" + if errors.Is(err, os.ErrNotExist) { + code = "UPLOAD_OBJECT_MISSING" + } + return RemoteOutcome{}, FailedExecution(code, + fmt.Sprintf("cannot read node upload object upload_id=%s: %v", snapshot.UploadID, err)) + } + if digest != snapshot.SHA256 || size != snapshot.SizeBytes { + return RemoteOutcome{}, FailedExecution("UPLOAD_OBJECT_MISMATCH", + fmt.Sprintf("node upload object does not match frozen metadata: upload_id=%s expected_size=%d size=%d expected_digest=%s digest=%s", + snapshot.UploadID, snapshot.SizeBytes, size, snapshot.SHA256, digest)) + } + + repository := &po.SoftwareRepository{ID: spec.GetRepositoryId(), PublishKey: spec.GetPublishKey(), + Architecture: spec.GetArchitecture(), GPGPolicy: spec.GetGpgPolicy(), + PublishBaseURLSnapshot: spec.GetPublishBaseUrl()} + applyMetadataSigningReference(repository, spec.GetMetadataSigning()) + task := &po.RepositoryTask{ID: spec.GetTaskId(), RepositoryID: spec.GetRepositoryId(), + StorageNodeID: spec.GetStorageNodeId(), PayloadJSON: spec.GetPayloadJson(), + WorkerID: "remote-data-plane", FencingToken: spec.GetFencingToken()} + upload := &po.RepositoryUpload{ID: snapshot.UploadID, RepositoryID: spec.GetRepositoryId(), + StorageNodeID: spec.GetStorageNodeId(), TaskID: spec.GetTaskId(), FileName: snapshot.FileName, + Kind: snapshot.Kind, ExpectedSize: snapshot.SizeBytes, ReceivedSize: snapshot.SizeBytes, + ActualSHA256: snapshot.SHA256, RelativePath: snapshot.RelativePath} + handler := &Handler{store: e.store, rpm: e.rpm, metadata: e.metadata, now: e.now} + candidates, err := handler.collectCandidates(ctx, task, upload) + if err != nil { + quarantined, quarantineErr := handler.quarantineFile(task, upload, upload.RelativePath, + upload.FileName, request.RepositoryQuarantineDangerousArchive, err.Error(), "") + if quarantineErr != nil { + return RemoteOutcome{}, FailedExecution("QUARANTINE_FAILED", quarantineErr.Error()) + } + return e.failedUploadResult(task, upload, handler, []po.RepositoryQuarantineItem{*quarantined}, + "DANGEROUS_ARCHIVE", err.Error()) + } + parent, base, err := e.loadActiveInventory(ctx, spec, repository) + if err != nil { + return RemoteOutcome{}, FailedExecution("ACTIVE_REVISION_INVENTORY_INVALID", err.Error()) + } + accepted, rejected, duplicates := e.validateCandidates(ctx, repository, candidates, base, verifier) + quarantine := make([]po.RepositoryQuarantineItem, 0, len(rejected)) + for _, item := range rejected { + quarantined, quarantineErr := handler.quarantineFile(task, upload, item.RelativePath, + item.OriginalName, item.RejectCode, item.RejectReason, item.NEVRA) + if quarantineErr != nil { + return RemoteOutcome{}, FailedExecution("QUARANTINE_FAILED", quarantineErr.Error()) + } + quarantine = append(quarantine, *quarantined) + } + if len(accepted) == 0 { + if len(rejected) > 0 { + message := fmt.Sprintf("all %d RPM packages were rejected; reasons=%s", + len(rejected), rejectionSummary(rejected)) + return e.failedUploadResult(task, upload, handler, quarantine, "ALL_PACKAGES_REJECTED", message) + } + return RemoteOutcome{}, FailedExecution("NO_PACKAGE_CHANGES", + fmt.Sprintf("all %d RPM packages already exist with identical content", duplicates)) + } + + revision, packages, privateRelative, err := handler.materializeRevision(task, repository, parent, base, accepted) + if err != nil { + return RemoteOutcome{}, FailedExecution("REVISION_BUILD_FAILED", err.Error()) + } + finalRelative, published, err := handler.materializePublishedRevision(ctx, task, repository, revision, packages) + if err != nil { + handler.moveRevisionToTrash(privateRelative, revision.ID) + return RemoteOutcome{}, FailedExecution("REVISION_PUBLISH_FAILED", err.Error()) + } + status := repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_SUCCESS + statusText := request.RepositoryTaskStatusSuccess + errorCode, errorMessage := "", "" + if len(rejected) > 0 { + status = repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_PARTIAL_SUCCESS + statusText = request.RepositoryTaskStatusPartialSuccess + errorCode = "SOME_PACKAGES_REJECTED" + errorMessage = fmt.Sprintf("accepted=%d rejected=%d reasons=%s", len(accepted), len(rejected), + rejectionSummary(rejected)) + } + resultObjectID, resultObjectSHA, err := writeRemoteResultObject(e.store, task, revision, published, + taskresult.WriteOptions{CompletionStatus: statusText, AcceptedCount: len(accepted), + RejectedCount: len(rejected), ErrorCode: errorCode, ErrorMessage: errorMessage, + Quarantine: quarantine}) + if err != nil { + handler.movePublishedToTrash(finalRelative, revision.ID) + handler.moveRevisionToTrash(privateRelative, revision.ID) + return RemoteOutcome{}, FailedExecution("TASK_RESULT_WRITE_FAILED", err.Error()) + } + handler.cleanupUploadStaging(upload, task) + return RemoteOutcome{RevisionID: revision.ID, ManifestSHA256: revision.SnapshotManifestSHA256, + ResultObjectID: resultObjectID, ResultObjectSHA256: resultObjectSHA, + PackageCount: int64(revision.PackageCount), TotalBytes: revision.TotalBytes, + CompletionStatus: status, ErrorCode: errorCode, ErrorMessage: errorMessage}, nil +} + +func (e *RemoteUploadExecutor) failedUploadResult(task *po.RepositoryTask, upload *po.RepositoryUpload, + handler *Handler, quarantine []po.RepositoryQuarantineItem, code, message string, +) (RemoteOutcome, error) { + objectID, objectSHA, err := writeRemoteFailureObject(e.store, task, upload, quarantine, code, message) + if err != nil { + return RemoteOutcome{}, FailedExecution("TASK_RESULT_WRITE_FAILED", err.Error()) + } + handler.cleanupUploadStaging(upload, task) + return RemoteOutcome{FailureResultObjectID: objectID, FailureResultObjectSHA256: objectSHA, + QuarantineCount: int64(len(quarantine))}, FailedExecution(code, message) +} + +func (e *RemoteUploadExecutor) Activate(_ context.Context, spec *repositorynodepb.RepositoryTaskSpec, + outcome RemoteOutcome, +) error { + if spec == nil || uuid.Validate(outcome.RevisionID) != nil { + return fmt.Errorf("remote upload activation identity is invalid") + } + if _, err := e.store.SwitchCurrent(spec.GetPublishKey(), outcome.RevisionID); err != nil { + return err + } + return e.store.VerifyPublishedCurrent(spec.GetPublishKey(), outcome.RevisionID) +} + +func decodeUploadSnapshot(payload string) (request.RepositoryUploadSnapshot, error) { + var value request.RepositoryUploadSnapshot + decoder := json.NewDecoder(strings.NewReader(payload)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&value); err != nil { + return value, fmt.Errorf("decode upload snapshot: %w", err) + } + if decoder.Decode(new(interface{})) != io.EOF { + return value, errors.New("upload snapshot contains trailing JSON data") + } + if uuid.Validate(value.UploadID) != nil || value.SizeBytes <= 0 || len(value.SHA256) != 64 { + return value, errors.New("upload snapshot identity, size, or digest is invalid") + } + if _, err := hex.DecodeString(value.SHA256); err != nil || strings.ToLower(value.SHA256) != value.SHA256 { + return value, errors.New("upload snapshot digest must be lowercase SHA-256") + } + if value.FileName == "" || filepath.Base(value.FileName) != value.FileName || + strings.IndexFunc(value.FileName, unicode.IsControl) >= 0 { + return value, errors.New("upload snapshot file name is invalid") + } + if value.Kind != request.RepositoryUploadKindRPM && value.Kind != request.RepositoryUploadKindTarGZ { + return value, errors.New("upload snapshot kind is invalid") + } + lowerName := strings.ToLower(value.FileName) + if value.Kind == request.RepositoryUploadKindRPM && !strings.HasSuffix(lowerName, ".rpm") { + return value, errors.New("RPM upload file name must end with .rpm") + } + if value.Kind == request.RepositoryUploadKindTarGZ && !strings.HasSuffix(lowerName, ".tar.gz") { + return value, errors.New("tar upload file name must end with .tar.gz") + } + return value, nil +} + +func (e *RemoteUploadExecutor) validateCandidates(ctx context.Context, repository *po.SoftwareRepository, + candidates []candidate, base []po.RepositoryPackage, verifier *signatureSession, +) (accepted, rejected []candidate, duplicates int) { + existing := make(map[string]po.RepositoryPackage, len(base)) + for _, item := range base { + existing[item.NEVRA] = item + } + for index := range candidates { + item := &candidates[index] + path, err := e.store.Resolve(item.RelativePath) + if err == nil { + item.SHA256, item.Size, err = fileSHA256(path) + } + if err == nil { + item.Info, err = e.rpm.Inspect(ctx, path) + } + if err != nil { + item.RejectCode, item.RejectReason = request.RepositoryQuarantineDamagedRPM, err.Error() + rejected = append(rejected, *item) + continue + } + if verifyErr := evaluateCandidateSignature(ctx, verifier, path, item); verifyErr != nil { + var signatureErr *signatureVerificationError + if errors.As(verifyErr, &signatureErr) { + item.RejectCode, item.RejectReason = signatureErr.Code, signatureErr.Message + } else { + item.RejectCode, item.RejectReason = request.RepositoryQuarantineBadSignature, verifyErr.Error() + } + rejected = append(rejected, *item) + continue + } + if !architectureAllowed(repository.Architecture, item.Info.Architecture) { + item.RejectCode, item.RejectReason = request.RepositoryQuarantineWrongArch, + fmt.Sprintf("package architecture %s is not allowed in %s repository", + item.Info.Architecture, repository.Architecture) + rejected = append(rejected, *item) + continue + } + item.NEVRA = formatNEVRA(item.Info) + if previous, exists := existing[item.NEVRA]; exists { + if previous.SHA256 == item.SHA256 { + duplicates++ + continue + } + item.RejectCode, item.RejectReason = request.RepositoryQuarantineNEVRAConflict, + "the same NEVRA already exists with a different SHA-256" + rejected = append(rejected, *item) + continue + } + existing[item.NEVRA] = po.RepositoryPackage{NEVRA: item.NEVRA, SHA256: item.SHA256} + accepted = append(accepted, *item) + } + return accepted, rejected, duplicates +} + +func newRemoteSignatureSession(ctx context.Context, store *storage.Store, + spec *repositorynodepb.RepositoryTaskSpec, +) (*signatureSession, error) { + if spec.GetGpgPolicy() == request.RepositoryGPGDisabled { + return nil, nil + } + if !request.RepositoryGPGPolicyEnabled(spec.GetGpgPolicy()) { + return nil, fmt.Errorf("unsupported repository GPG policy %q", spec.GetGpgPolicy()) + } + references := make([]signatureKeyReference, 0, len(spec.GetGpgKeys())) + for _, key := range spec.GetGpgKeys() { + if key == nil { + return nil, errors.New("repository task contains a nil GPG key reference") + } + references = append(references, signatureKeyReference{KeyID: key.GetKeyId(), + Fingerprint: key.GetFingerprint(), PublicKeyPath: key.GetPublicKeyPath()}) + } + return newSignatureSessionFromReferencesWithPolicy(ctx, store, spec.GetTaskId(), spec.GetRepositoryId(), + spec.GetPublishKey(), spec.GetGpgPolicy(), references) +} + +func (e *RemoteUploadExecutor) loadActiveInventory(ctx context.Context, spec *repositorynodepb.RepositoryTaskSpec, + repository *po.SoftwareRepository, +) (*po.RepositoryRevision, []po.RepositoryPackage, error) { + return loadRemoteActiveInventory(ctx, e.store, e.rpm, spec, repository) +} + +func loadRemoteActiveInventory(ctx context.Context, store *storage.Store, rpm RPMTool, + spec *repositorynodepb.RepositoryTaskSpec, repository *po.SoftwareRepository, +) (*po.RepositoryRevision, []po.RepositoryPackage, error) { + if spec.GetActiveRevisionId() == "" { + return nil, nil, nil + } + if uuid.Validate(spec.GetActiveRevisionId()) != nil { + return nil, nil, errors.New("active revision identity is invalid") + } + relative, err := store.PublicRevisionRelativePath(spec.GetPublishKey(), spec.GetActiveRevisionId()) + if err != nil { + return nil, nil, err + } + manifestPath, err := store.Resolve(filepath.ToSlash(filepath.Join(relative, "manifest.json"))) + if err != nil { + return nil, nil, err + } + file, err := os.Open(manifestPath) + if err != nil { + return nil, nil, err + } + defer file.Close() + info, err := file.Stat() + if err != nil { + return nil, nil, fmt.Errorf("stat active revision manifest: %w", err) + } + if !info.Mode().IsRegular() || info.Size() <= 0 || info.Size() > maxPublishedManifestBytes { + return nil, nil, fmt.Errorf("active revision manifest size or type is invalid: size=%d", info.Size()) + } + decoder := json.NewDecoder(io.LimitReader(file, maxPublishedManifestBytes+1)) + decoder.DisallowUnknownFields() + var manifest publishedManifest + if err = decoder.Decode(&manifest); err != nil { + return nil, nil, fmt.Errorf("decode active revision manifest: %w", err) + } + if decoder.Decode(new(interface{})) != io.EOF || manifest.SchemaVersion != 3 || + manifest.RepositoryID != repository.ID || manifest.RevisionID != spec.GetActiveRevisionId() { + return nil, nil, errors.New("active revision manifest identity or schema is invalid") + } + packages := make([]po.RepositoryPackage, 0, len(manifest.Packages)) + seen := make(map[string]struct{}, len(manifest.Packages)) + for _, item := range manifest.Packages { + if filepath.Base(item.Path) != item.Path || !strings.HasSuffix(strings.ToLower(item.Path), ".rpm") || + filepath.Base(item.FileName) != item.FileName || item.SizeBytes <= 0 || len(item.SHA256) != 64 { + return nil, nil, fmt.Errorf("active package %q has invalid manifest metadata", item.NEVRA) + } + packageRelative := filepath.ToSlash(filepath.Join(relative, item.Path)) + packagePath, resolveErr := store.Resolve(packageRelative) + if resolveErr != nil { + return nil, nil, resolveErr + } + digest, size, hashErr := fileSHA256(packagePath) + if hashErr != nil || digest != item.SHA256 || size != item.SizeBytes { + return nil, nil, fmt.Errorf("active package %q does not match manifest", item.NEVRA) + } + info, inspectErr := rpm.Inspect(ctx, packagePath) + if inspectErr != nil || info.Name != item.Name || normalizeEpoch(info.Epoch) != item.Epoch || + info.Version != item.Version || info.Release != item.Release || + info.Architecture != item.Architecture || formatNEVRA(info) != item.NEVRA || + !architectureAllowed(repository.Architecture, info.Architecture) { + return nil, nil, fmt.Errorf("active package %q RPM metadata does not match manifest", item.NEVRA) + } + if item.Source != request.RepositoryPackageSourceSync && item.Source != request.RepositoryPackageSourceUpload { + return nil, nil, fmt.Errorf("active package %q source is invalid", item.NEVRA) + } + if item.Source == request.RepositoryPackageSourceUpload && uuid.Validate(item.SourceUploadID) != nil { + return nil, nil, fmt.Errorf("active package %q upload identity is invalid", item.NEVRA) + } + if item.Source == request.RepositoryPackageSourceSync && item.SourceUploadID != "" { + return nil, nil, fmt.Errorf("active package %q contains an unexpected upload identity", item.NEVRA) + } + if _, exists := seen[item.NEVRA]; exists { + return nil, nil, fmt.Errorf("active revision manifest contains duplicate NEVRA %q", item.NEVRA) + } + seen[item.NEVRA] = struct{}{} + packages = append(packages, po.RepositoryPackage{ID: uuid.NewString(), RepositoryID: repository.ID, + RevisionID: spec.GetActiveRevisionId(), Name: item.Name, Epoch: item.Epoch, Version: item.Version, + Release: item.Release, Architecture: item.Architecture, NEVRA: item.NEVRA, FileName: item.FileName, + RelativePath: packageRelative, SizeBytes: item.SizeBytes, SHA256: item.SHA256, + Source: item.Source, SourceUploadID: item.SourceUploadID, SignatureStatus: item.SignatureStatus, + SignerKeyID: item.SignerKeyID, SignatureMessage: item.SignatureMessage}) + } + return &po.RepositoryRevision{ID: spec.GetActiveRevisionId(), RepositoryID: repository.ID, + StorageNodeID: spec.GetStorageNodeId(), RelativePath: relative}, packages, nil +} + +var _ RemoteExecutor = (*RemoteUploadExecutor)(nil) diff --git a/manager/backend/services/footstone/repositoryworker/remote_upload_executor_test.go b/manager/backend/services/footstone/repositoryworker/remote_upload_executor_test.go new file mode 100644 index 0000000000000000000000000000000000000000..563920158be80621801a81108e18f1f0b2e2c65a --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/remote_upload_executor_test.go @@ -0,0 +1,357 @@ +package repositoryworker + +import ( + "archive/tar" + "compress/gzip" + "context" + "encoding/json" + "errors" + "os" + "path/filepath" + "strings" + "testing" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + repositorynodepb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repositorynode" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/taskresult" +) + +const remoteUploadID = "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee" + +func TestRemoteUploadExecutorPublishesPartialTarResult(t *testing.T) { + root := t.TempDir() + t.Cleanup(func() { makeRemoteTestTreeWritable(root) }) + store, err := storage.New(config.RepositoryConfig{Root: root, MetadataReserve: 1, + MinimumFreeBytes: 1, MaxExpandedBytes: 1 << 20, MaxArchiveFiles: 10}) + if err != nil { + t.Fatal(err) + } + archive := writeRemoteUploadArchive(t, store, remoteUploadID) + digest, size, err := fileSHA256(archive) + if err != nil { + t.Fatal(err) + } + snapshot, _ := json.Marshal(request.RepositoryUploadSnapshot{UploadID: remoteUploadID, + FileName: "private-fixtures.tar.gz", Kind: request.RepositoryUploadKindTarGZ, + RelativePath: store.UploadRelativePath(remoteUploadID), SizeBytes: size, SHA256: digest}) + spec := &repositorynodepb.RepositoryTaskSpec{TaskId: remoteTestTaskID, RepositoryId: remoteTestRepoID, + StorageNodeId: remoteTestNodeID, TaskType: repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_UPLOAD, + FencingToken: 12, PayloadJson: string(snapshot), PublishKey: "oc9-private", + Architecture: request.RepositoryArchitectureX8664, GpgPolicy: request.RepositoryGPGDisabled} + executor := &RemoteUploadExecutor{store: store, rpm: remoteUploadFakeRPM{}, + metadata: remoteFakeMetadata{}, now: testNow} + outcome, err := executor.Execute(context.Background(), spec) + if err != nil { + t.Fatal(err) + } + if outcome.CompletionStatus != repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_PARTIAL_SUCCESS || + outcome.ErrorCode != "SOME_PACKAGES_REJECTED" { + t.Fatalf("partial upload outcome = %#v", outcome) + } + if _, err = os.Lstat(filepath.Join(root, "public", spec.GetPublishKey(), "current")); !os.IsNotExist(err) { + t.Fatalf("current link exists before activation: %v", err) + } + resultPath, err := store.Resolve(outcome.ResultObjectID) + if err != nil { + t.Fatal(err) + } + result, err := os.Open(resultPath) + if err != nil { + t.Fatal(err) + } + document, parseErr := taskresult.ParseZstd(result, taskresult.Expectation{TaskID: spec.GetTaskId(), + RepositoryID: spec.GetRepositoryId(), StorageNodeID: spec.GetStorageNodeId(), + RevisionID: outcome.RevisionID, PublishKey: spec.GetPublishKey(), ManifestSHA256: outcome.ManifestSHA256, + Kind: request.RepositoryRevisionKindUpload, Architecture: spec.GetArchitecture(), + PackageCount: outcome.PackageCount, TotalBytes: outcome.TotalBytes, + CompletionStatus: request.RepositoryTaskStatusPartialSuccess, UploadID: remoteUploadID}) + _ = result.Close() + if parseErr != nil { + t.Fatal(parseErr) + } + if len(document.Packages) != 1 || len(document.Quarantine) != 1 || + document.Packages[0].SourceUploadID != remoteUploadID || + document.Quarantine[0].ReasonCode != request.RepositoryQuarantineDamagedRPM { + t.Fatalf("partial upload result = %#v", document) + } + quarantinePath, err := store.Resolve(document.Quarantine[0].RelativePath) + if err != nil { + t.Fatal(err) + } + if _, err = os.Stat(quarantinePath); err != nil { + t.Fatalf("quarantined package is missing: %v", err) + } + if err = executor.Activate(context.Background(), spec, outcome); err != nil { + t.Fatal(err) + } +} + +func TestRemoteUploadExecutorFailsClosedForUnsynchronizedGPGPolicy(t *testing.T) { + store, err := storage.New(config.RepositoryConfig{Root: t.TempDir(), MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + executor := &RemoteUploadExecutor{store: store, rpm: remoteUploadFakeRPM{}, + metadata: remoteFakeMetadata{}, now: testNow} + _, err = executor.Execute(context.Background(), &repositorynodepb.RepositoryTaskSpec{ + TaskId: remoteTestTaskID, RepositoryId: remoteTestRepoID, StorageNodeId: remoteTestNodeID, + TaskType: repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_UPLOAD, + PublishKey: "private", GpgPolicy: request.RepositoryGPGVerifyPackage, + }) + var failure *ExecutionError + if !errors.As(err, &failure) || failure.Code != "GPG_KEY_SYNC_REQUIRED" { + t.Fatalf("Execute() GPG error = %v", err) + } +} + +func TestRemoteUploadExecutorClassifiesObjectReadAndIntegrityFailures(t *testing.T) { + for _, fixture := range []struct { + name string + kind string + code string + }{ + {name: "missing object", kind: "missing", code: "UPLOAD_OBJECT_MISSING"}, + {name: "unreadable object", kind: "directory", code: "UPLOAD_OBJECT_UNREADABLE"}, + {name: "empty object", kind: "empty", code: "UPLOAD_OBJECT_MISMATCH"}, + {name: "changed content", kind: "changed", code: "UPLOAD_OBJECT_MISMATCH"}, + } { + t.Run(fixture.name, func(t *testing.T) { + store, err := storage.New(config.RepositoryConfig{Root: t.TempDir(), MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + relative, err := store.CreateUpload(remoteUploadID, 8) + if err != nil { + t.Fatal(err) + } + objectPath, err := store.Resolve(relative) + if err != nil { + t.Fatal(err) + } + switch fixture.kind { + case "missing", "directory": + if err = os.Remove(objectPath); err != nil { + t.Fatal(err) + } + if fixture.kind == "directory" { + if err = os.Mkdir(objectPath, 0700); err != nil { + t.Fatal(err) + } + } + case "changed": + if err = os.WriteFile(objectPath, []byte("evil-rpm"), 0600); err != nil { + t.Fatal(err) + } + } + payload, err := json.Marshal(request.RepositoryUploadSnapshot{ + UploadID: remoteUploadID, FileName: "same-name.rpm", Kind: request.RepositoryUploadKindRPM, + RelativePath: relative, SizeBytes: 8, SHA256: strings.Repeat("a", 64), + }) + if err != nil { + t.Fatal(err) + } + executor := &RemoteUploadExecutor{store: store, rpm: remoteUploadFakeRPM{}, + metadata: remoteFakeMetadata{}, now: testNow} + spec := &repositorynodepb.RepositoryTaskSpec{ + TaskId: remoteTestTaskID, RepositoryId: remoteTestRepoID, StorageNodeId: remoteTestNodeID, + TaskType: repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_UPLOAD, + PublishKey: "private", GpgPolicy: request.RepositoryGPGDisabled, PayloadJson: string(payload), + } + outcome, executeErr := executor.Execute(context.Background(), spec) + var failure *ExecutionError + if !errors.As(executeErr, &failure) || failure.Code != fixture.code { + t.Fatalf("Execute() error = %v, want %s", executeErr, fixture.code) + } + if !strings.Contains(failure.Message, remoteUploadID) { + t.Fatalf("error omitted the upload identity: %v", failure) + } + if fixture.kind == "missing" || fixture.kind == "directory" { + if strings.Contains(failure.Message, "size=0 digest=") || + !strings.Contains(failure.Message, "cannot read node upload object") { + t.Fatalf("read failure was reported as an empty object: %v", failure) + } + } else if !strings.Contains(failure.Message, "expected_size=8") || + !strings.Contains(failure.Message, "expected_digest=") { + t.Fatalf("integrity failure omitted the frozen expectations: %v", failure) + } + if outcome.RevisionID != "" || outcome.ResultObjectID != "" { + t.Fatalf("invalid upload produced a revision: %#v", outcome) + } + if _, err = os.Lstat(filepath.Join(store.Root(), "public", spec.GetPublishKey(), "current")); !os.IsNotExist(err) { + t.Fatalf("invalid upload changed the published revision: %v", err) + } + }) + } +} + +func TestRemoteUploadExecutorReportsAllRejectedWithoutActivation(t *testing.T) { + root := t.TempDir() + t.Cleanup(func() { makeRemoteTestTreeWritable(root) }) + store, err := storage.New(config.RepositoryConfig{Root: root, MetadataReserve: 1, MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + relative := store.UploadRelativePath(remoteUploadID) + path, err := store.Resolve(relative) + if err != nil { + t.Fatal(err) + } + if err = os.MkdirAll(filepath.Dir(path), 0700); err != nil { + t.Fatal(err) + } + if err = os.WriteFile(path, []byte("bad-rpm"), 0600); err != nil { + t.Fatal(err) + } + digest, size, err := fileSHA256(path) + if err != nil { + t.Fatal(err) + } + snapshot, _ := json.Marshal(request.RepositoryUploadSnapshot{UploadID: remoteUploadID, + FileName: "damaged.rpm", Kind: request.RepositoryUploadKindRPM, RelativePath: relative, + SizeBytes: size, SHA256: digest}) + spec := &repositorynodepb.RepositoryTaskSpec{TaskId: remoteTestTaskID, RepositoryId: remoteTestRepoID, + StorageNodeId: remoteTestNodeID, TaskType: repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_UPLOAD, + FencingToken: 13, PayloadJson: string(snapshot), PublishKey: "oc9-private", + Architecture: request.RepositoryArchitectureX8664, GpgPolicy: request.RepositoryGPGDisabled} + executor := &RemoteUploadExecutor{store: store, rpm: remoteUploadFakeRPM{}, + metadata: remoteFakeMetadata{}, now: testNow} + outcome, executeErr := executor.Execute(context.Background(), spec) + var failure *ExecutionError + if !errors.As(executeErr, &failure) || failure.Code != "ALL_PACKAGES_REJECTED" || + outcome.QuarantineCount != 1 || outcome.FailureResultObjectID == "" { + t.Fatalf("failed upload outcome/error = %#v/%v", outcome, executeErr) + } + resultPath, err := store.Resolve(outcome.FailureResultObjectID) + if err != nil { + t.Fatal(err) + } + result, err := os.Open(resultPath) + if err != nil { + t.Fatal(err) + } + document, parseErr := taskresult.ParseFailureZstd(result, taskresult.FailureExpectation{ + TaskID: spec.GetTaskId(), RepositoryID: spec.GetRepositoryId(), StorageNodeID: spec.GetStorageNodeId(), + UploadID: remoteUploadID, ErrorCode: failure.Code, QuarantineCount: 1}) + _ = result.Close() + if parseErr != nil { + t.Fatal(parseErr) + } + if len(document.Quarantine) != 1 || document.Quarantine[0].ReasonCode != request.RepositoryQuarantineDamagedRPM { + t.Fatalf("failure result = %#v", document) + } + if _, err = os.Lstat(filepath.Join(root, "public", spec.GetPublishKey(), "current")); !os.IsNotExist(err) { + t.Fatalf("failed upload changed current: %v", err) + } +} + +func TestDecodeUploadSnapshotRejectsKindExtensionMismatch(t *testing.T) { + tests := []request.RepositoryUploadSnapshot{ + {UploadID: remoteUploadID, FileName: "private.tar.gz", Kind: request.RepositoryUploadKindRPM, + SizeBytes: 1, SHA256: strings.Repeat("a", 64)}, + {UploadID: remoteUploadID, FileName: "private.rpm", Kind: request.RepositoryUploadKindTarGZ, + SizeBytes: 1, SHA256: strings.Repeat("a", 64)}, + } + for _, value := range tests { + payload, err := json.Marshal(value) + if err != nil { + t.Fatal(err) + } + if _, err = decodeUploadSnapshot(string(payload)); err == nil { + t.Fatalf("decodeUploadSnapshot(%s, %s) accepted mismatched extension", value.Kind, value.FileName) + } + } +} + +func TestRemoteUploadExecutorRejectsOversizedActiveManifest(t *testing.T) { + store, err := storage.New(config.RepositoryConfig{Root: t.TempDir(), MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + revisionID := "dddddddd-dddd-4ddd-8ddd-dddddddddddd" + relative, err := store.PublicRevisionRelativePath("private", revisionID) + if err != nil { + t.Fatal(err) + } + manifestPath, err := store.Resolve(filepath.ToSlash(filepath.Join(relative, "manifest.json"))) + if err != nil { + t.Fatal(err) + } + if err = os.MkdirAll(filepath.Dir(manifestPath), 0700); err != nil { + t.Fatal(err) + } + manifest, err := os.OpenFile(manifestPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) + if err != nil { + t.Fatal(err) + } + if err = manifest.Truncate(maxPublishedManifestBytes + 1); err != nil { + _ = manifest.Close() + t.Fatal(err) + } + if err = manifest.Close(); err != nil { + t.Fatal(err) + } + executor := &RemoteUploadExecutor{store: store, rpm: remoteUploadFakeRPM{}, + metadata: remoteFakeMetadata{}, now: testNow} + spec := &repositorynodepb.RepositoryTaskSpec{RepositoryId: remoteTestRepoID, + StorageNodeId: remoteTestNodeID, PublishKey: "private", ActiveRevisionId: revisionID} + _, _, err = executor.loadActiveInventory(context.Background(), spec, + &po.SoftwareRepository{ID: remoteTestRepoID, Architecture: request.RepositoryArchitectureX8664}) + if err == nil || !strings.Contains(err.Error(), "manifest size or type is invalid") { + t.Fatalf("loadActiveInventory() oversized manifest error = %v", err) + } +} + +func writeRemoteUploadArchive(t *testing.T, store *storage.Store, uploadID string) string { + t.Helper() + relative := store.UploadRelativePath(uploadID) + path, err := store.Resolve(relative) + if err != nil { + t.Fatal(err) + } + if err = os.MkdirAll(filepath.Dir(path), 0700); err != nil { + t.Fatal(err) + } + file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) + if err != nil { + t.Fatal(err) + } + gz := gzip.NewWriter(file) + archive := tar.NewWriter(gz) + for name, body := range map[string]string{"private-good.rpm": "good-rpm", "private-bad.rpm": "bad-rpm"} { + if err = archive.WriteHeader(&tar.Header{Name: name, Mode: 0600, Size: int64(len(body)), + Typeflag: tar.TypeReg}); err != nil { + t.Fatal(err) + } + if _, err = archive.Write([]byte(body)); err != nil { + t.Fatal(err) + } + } + if err = archive.Close(); err != nil { + t.Fatal(err) + } + if err = gz.Close(); err != nil { + t.Fatal(err) + } + if err = file.Close(); err != nil { + t.Fatal(err) + } + return path +} + +type remoteUploadFakeRPM struct{} + +func (remoteUploadFakeRPM) Inspect(_ context.Context, path string) (RPMInfo, error) { + body, err := os.ReadFile(path) + if err != nil { + return RPMInfo{}, err + } + if string(body) != "good-rpm" { + return RPMInfo{}, errors.New("rpm integrity verification failed") + } + return RPMInfo{Name: "ocm-private-only", Epoch: "0", Version: "1.0", Release: "1", + Architecture: request.RepositoryArchitectureNoarch}, nil +} diff --git a/manager/backend/services/footstone/repositoryworker/remote_worker.go b/manager/backend/services/footstone/repositoryworker/remote_worker.go new file mode 100644 index 0000000000000000000000000000000000000000..00ef78aa1c379b582ec7c0b5c2f63b5352fe5725 --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/remote_worker.go @@ -0,0 +1,375 @@ +package repositoryworker + +import ( + "context" + "encoding/hex" + "errors" + "fmt" + "strings" + "time" + + "github.com/google/uuid" + "trpc.group/trpc-go/trpc-go/log" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + repositorynodepb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repositorynode" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/repositoryworker/controlplane" +) + +type NodeProbe interface { + Probe(context.Context) (controlplane.NodeHeartbeat, error) +} + +type RemoteExecutor interface { + Execute(context.Context, *repositorynodepb.RepositoryTaskSpec) (RemoteOutcome, error) + Activate(context.Context, *repositorynodepb.RepositoryTaskSpec, RemoteOutcome) error +} + +type RemoteOutcome struct { + RevisionID string + ManifestSHA256 string + ResultObjectID string + ResultObjectSHA256 string + PackageCount int64 + TotalBytes int64 + CompletionStatus repositorypb.RepositoryTaskStatus + ErrorCode string + ErrorMessage string + FailureResultObjectID string + FailureResultObjectSHA256 string + QuarantineCount int64 +} + +type ExecutionError struct { + Code string + Message string +} + +func (e *ExecutionError) Error() string { + if e == nil { + return "" + } + if e.Message == "" { + return e.Code + } + return e.Message +} + +func FailedExecution(code, message string) error { + return &ExecutionError{Code: code, Message: message} +} + +type RemoteWorker struct { + nodeID string + workerID string + control controlplane.Plane + probe NodeProbe + executor RemoteExecutor + lease time.Duration + pollInterval time.Duration + heartbeatInterval time.Duration +} + +func NewRemoteWorker(nodeID, workerID string, control controlplane.Plane, probe NodeProbe, + executor RemoteExecutor, lease, pollInterval, heartbeatInterval time.Duration, +) (*RemoteWorker, error) { + if uuid.Validate(nodeID) != nil { + return nil, fmt.Errorf("remote repository worker requires a valid storage node id") + } + if strings.TrimSpace(workerID) == "" || control == nil || probe == nil || executor == nil { + return nil, fmt.Errorf("remote repository worker requires worker id, control plane, probe, and executor") + } + if lease < 15*time.Second || lease > 300*time.Second { + return nil, fmt.Errorf("remote repository worker lease must be between 15 and 300 seconds") + } + if pollInterval <= 0 { + pollInterval = time.Second + } + if heartbeatInterval <= 0 { + heartbeatInterval = 10 * time.Second + } + return &RemoteWorker{nodeID: nodeID, workerID: workerID, control: control, probe: probe, + executor: executor, lease: lease, pollInterval: pollInterval, heartbeatInterval: heartbeatInterval}, nil +} + +func (w *RemoteWorker) Run(ctx context.Context) error { + // Wait for the initial heartbeat before claiming tasks, but keep the worker + // alive when the control plane is temporarily unavailable during startup. + for { + if ctx.Err() != nil { + return nil + } + if err := w.sendNodeHeartbeat(ctx); err == nil { + break + } else { + log.Errorf("initial repository node heartbeat: %v", err) + } + if !waitContext(ctx, w.heartbeatInterval) { + return nil + } + } + heartbeatCtx, stopHeartbeat := context.WithCancel(ctx) + defer stopHeartbeat() + go w.nodeHeartbeatLoop(heartbeatCtx) + for { + if ctx.Err() != nil { + return nil + } + task, err := w.control.ClaimTask(ctx, w.workerID, int32(w.lease/time.Second)) + if err != nil { + log.Errorf("remote repository worker claim task: %v", err) + if !waitContext(ctx, w.pollInterval) { + return nil + } + continue + } + if task == nil { + if !waitContext(ctx, w.pollInterval) { + return nil + } + continue + } + if err = w.ExecuteClaimed(ctx, task); err != nil { + log.Errorf("remote repository worker task %s: %v", task.GetTaskId(), err) + } + } +} + +func (w *RemoteWorker) ExecuteClaimed(parent context.Context, task *repositorynodepb.RepositoryTaskSpec) error { + if task == nil || task.GetTaskId() == "" || task.GetRepositoryId() == "" { + return fmt.Errorf("remote repository task spec is incomplete") + } + if task.GetStorageNodeId() != w.nodeID { + return fmt.Errorf("remote repository task belongs to another storage node") + } + ctx, cancel := context.WithCancel(parent) + defer cancel() + stop := make(chan struct{}) + heartbeatResult := make(chan error, 1) + cancelRequested := make(chan struct{}, 1) + go w.taskHeartbeatLoop(ctx, task, stop, heartbeatResult, cancelRequested, cancel) + + outcome, executeErr := w.executor.Execute(ctx, task) + close(stop) + if heartbeatErr := <-heartbeatResult; heartbeatErr != nil { + return heartbeatErr + } + select { + case <-cancelRequested: + return w.reportFailure(parent, task, + repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_CANCELED, + "CANCELED_BY_USER", "repository task cancellation was requested", RemoteOutcome{}) + default: + } + if task.GetTaskType() == repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_DISTRIBUTE { + if executeErr != nil { + code, message := executionFailure(executeErr) + return w.reportFailure(parent, task, + repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_FAILED, code, message, RemoteOutcome{}) + } + switch outcome.CompletionStatus { + case repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_SUCCESS, + repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_PARTIAL_SUCCESS, + repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_FAILED, + repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_CANCELED: + return nil + default: + return fmt.Errorf("distribution control plane returned non-terminal status %s", + outcome.CompletionStatus.String()) + } + } + if executeErr != nil { + code, message := executionFailure(executeErr) + if err := validateRemoteFailureOutcome(outcome); err != nil { + code, message = "INVALID_TASK_FAILURE_RESULT", err.Error() + outcome = RemoteOutcome{} + } + return w.reportFailure(parent, task, + repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_FAILED, code, message, outcome) + } + if err := validateRemoteOutcome(outcome); err != nil { + return w.reportFailure(parent, task, + repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_FAILED, + "INVALID_TASK_RESULT", err.Error(), RemoteOutcome{}) + } + prepared, err := w.control.PrepareTaskCommit(parent, controlplane.PreparedRevision{ + TaskID: task.GetTaskId(), WorkerID: w.workerID, FencingToken: task.GetFencingToken(), + RevisionID: outcome.RevisionID, ManifestSHA256: outcome.ManifestSHA256, + ResultObjectID: outcome.ResultObjectID, ResultObjectSHA256: outcome.ResultObjectSHA256, + PackageCount: outcome.PackageCount, TotalBytes: outcome.TotalBytes, + CompletionStatus: outcome.CompletionStatus, ErrorCode: outcome.ErrorCode, + ErrorMessage: outcome.ErrorMessage, + }) + if err != nil { + return fmt.Errorf("prepare remote repository task commit: %w", err) + } + if prepared.GetRevisionId() != outcome.RevisionID || prepared.GetManifestSha256() != outcome.ManifestSHA256 || + prepared.GetCommitToken() == "" { + return fmt.Errorf("prepare remote repository task commit returned inconsistent identity") + } + if err = w.executor.Activate(parent, task, outcome); err != nil { + return w.reportFailure(parent, task, + repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_FAILED, + "REVISION_ACTIVATION_FAILED", err.Error(), RemoteOutcome{}) + } + if err = w.control.ConfirmTaskCommit(parent, controlplane.ConfirmedRevision{ + TaskID: task.GetTaskId(), WorkerID: w.workerID, FencingToken: task.GetFencingToken(), + CommitToken: prepared.GetCommitToken(), + }); err != nil { + return fmt.Errorf("confirm remote repository task commit: %w", err) + } + return nil +} + +func (w *RemoteWorker) reportFailure(ctx context.Context, task *repositorynodepb.RepositoryTaskSpec, + status repositorypb.RepositoryTaskStatus, code, message string, outcome RemoteOutcome, +) error { + err := w.control.ReportTaskFailure(ctx, controlplane.TaskFailure{ + TaskID: task.GetTaskId(), WorkerID: w.workerID, FencingToken: task.GetFencingToken(), Status: status, + ErrorCode: truncateText(strings.TrimSpace(code), 64), + ErrorMessage: truncateText(strings.TrimSpace(message), 2048), + ResultObjectID: outcome.FailureResultObjectID, + ResultObjectSHA256: outcome.FailureResultObjectSHA256, + QuarantineCount: outcome.QuarantineCount, + }) + if err != nil { + return fmt.Errorf("report remote repository task failure: %w", err) + } + return nil +} + +func validateRemoteFailureOutcome(value RemoteOutcome) error { + if value.FailureResultObjectID == "" && value.FailureResultObjectSHA256 == "" && value.QuarantineCount == 0 { + return nil + } + if strings.TrimSpace(value.FailureResultObjectID) == "" || value.QuarantineCount <= 0 || + !validSHA256(value.FailureResultObjectSHA256) { + return fmt.Errorf("failure result object identity and quarantine count are invalid") + } + return nil +} + +func (w *RemoteWorker) taskHeartbeatLoop(ctx context.Context, task *repositorynodepb.RepositoryTaskSpec, + stop <-chan struct{}, result chan<- error, cancelRequested chan<- struct{}, cancel context.CancelFunc, +) { + ticker := time.NewTicker(w.lease / 3) + defer ticker.Stop() + for { + select { + case <-stop: + result <- nil + return + case <-ctx.Done(): + select { + case <-stop: + result <- nil + default: + result <- ctx.Err() + } + return + case <-ticker.C: + response, err := w.control.HeartbeatTask(ctx, controlplane.TaskHeartbeat{ + TaskID: task.GetTaskId(), WorkerID: w.workerID, FencingToken: task.GetFencingToken(), + LeaseSeconds: int32(w.lease / time.Second), CurrentStep: "processing repository content", + }) + if err != nil { + cancel() + result <- fmt.Errorf("heartbeat remote repository task: %w", err) + return + } + if response.GetCancelRequested() { + select { + case cancelRequested <- struct{}{}: + default: + } + cancel() + result <- nil + return + } + } + } +} + +func (w *RemoteWorker) nodeHeartbeatLoop(ctx context.Context) { + ticker := time.NewTicker(w.heartbeatInterval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + if err := w.sendNodeHeartbeat(ctx); err != nil { + log.Errorf("repository node heartbeat: %v", err) + } + } + } +} + +func (w *RemoteWorker) sendNodeHeartbeat(ctx context.Context) error { + value, err := w.probe.Probe(ctx) + if err != nil { + return err + } + _, err = w.control.HeartbeatNode(ctx, value) + return err +} + +func executionFailure(err error) (string, string) { + var value *ExecutionError + if errors.As(err, &value) { + code := strings.TrimSpace(value.Code) + if code == "" { + code = "TASK_EXECUTION_FAILED" + } + return code, value.Error() + } + return "TASK_EXECUTION_FAILED", err.Error() +} + +func validateRemoteOutcome(value RemoteOutcome) error { + if uuid.Validate(value.RevisionID) != nil { + return fmt.Errorf("result revision_id is invalid") + } + for label, digest := range map[string]string{ + "manifest_sha256": value.ManifestSHA256, "result_object_sha256": value.ResultObjectSHA256, + } { + if !validSHA256(digest) { + return fmt.Errorf("result %s must be a SHA-256 digest", label) + } + } + if strings.TrimSpace(value.ResultObjectID) == "" || value.PackageCount < 0 || value.TotalBytes < 0 { + return fmt.Errorf("result object identity and counters are invalid") + } + if value.CompletionStatus != repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_SUCCESS && + value.CompletionStatus != repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_PARTIAL_SUCCESS { + return fmt.Errorf("result completion status must be SUCCESS or PARTIAL_SUCCESS") + } + if value.CompletionStatus == repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_SUCCESS && + (strings.TrimSpace(value.ErrorCode) != "" || strings.TrimSpace(value.ErrorMessage) != "") { + return fmt.Errorf("successful result must not contain an error summary") + } + if value.CompletionStatus == repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_PARTIAL_SUCCESS && + strings.TrimSpace(value.ErrorCode) == "" { + return fmt.Errorf("partial result requires an error code") + } + return nil +} + +func validSHA256(value string) bool { + if len(value) != 64 || strings.ToLower(value) != value { + return false + } + _, err := hex.DecodeString(value) + return err == nil +} + +func waitContext(ctx context.Context, duration time.Duration) bool { + timer := time.NewTimer(duration) + defer timer.Stop() + select { + case <-ctx.Done(): + return false + case <-timer.C: + return true + } +} diff --git a/manager/backend/services/footstone/repositoryworker/remote_worker_recovery_test.go b/manager/backend/services/footstone/repositoryworker/remote_worker_recovery_test.go new file mode 100644 index 0000000000000000000000000000000000000000..3b24ef82be32510402e7cc149bccb76760b58522 --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/remote_worker_recovery_test.go @@ -0,0 +1,280 @@ +package repositoryworker + +import ( + "context" + "errors" + "sync/atomic" + "testing" + "time" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + repositorynodepb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repositorynode" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/repositoryworker/controlplane" +) + +func TestRemoteWorkerRetriesInitialHeartbeatBeforeClaiming(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + var heartbeats, claims atomic.Int32 + plane := &recoveryRemotePlane{fakeRemotePlane: &fakeRemotePlane{}} + plane.heartbeatNode = func(context.Context, controlplane.NodeHeartbeat) (*repositorynodepb.HeartbeatNodeResponse, error) { + if heartbeats.Add(1) == 1 { + return nil, errors.New("control connection is closed") + } + return &repositorynodepb.HeartbeatNodeResponse{NodeEnabled: true}, nil + } + plane.claim = func(context.Context, string, int32) (*repositorynodepb.RepositoryTaskSpec, error) { + if heartbeats.Load() < 2 { + t.Error("claimed a task before the first successful heartbeat") + } + if claims.Add(1) == 1 { + return remoteTaskSpec(), nil + } + cancel() + return nil, nil + } + executor := &recoveryRemoteExecutor{execute: func(context.Context, *repositorynodepb.RepositoryTaskSpec) (RemoteOutcome, error) { + return validRemoteOutcome(), nil + }} + worker := newRecoveryRemoteWorker(t, plane, executor) + if err := worker.Run(ctx); err != nil { + t.Fatal(err) + } + if ctx.Err() == context.DeadlineExceeded || heartbeats.Load() < 2 || claims.Load() != 2 || + plane.prepares != 1 || plane.confirms != 1 || executor.activations.Load() != 1 { + t.Fatalf("startup recovery heartbeat/claim/prepare/confirm/activate = %d/%d/%d/%d/%d", + heartbeats.Load(), claims.Load(), plane.prepares, plane.confirms, executor.activations.Load()) + } +} + +func TestRemoteWorkerStartupHeartbeatRetryStopsOnCancellation(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + var heartbeats, claims atomic.Int32 + plane := &recoveryRemotePlane{fakeRemotePlane: &fakeRemotePlane{}} + plane.heartbeatNode = func(context.Context, controlplane.NodeHeartbeat) (*repositorynodepb.HeartbeatNodeResponse, error) { + if heartbeats.Add(1) == 2 { + cancel() + } + return nil, errors.New("control plane unavailable") + } + plane.claim = func(context.Context, string, int32) (*repositorynodepb.RepositoryTaskSpec, error) { + claims.Add(1) + return nil, nil + } + worker := newRecoveryRemoteWorker(t, plane, &recoveryRemoteExecutor{}) + if err := worker.Run(ctx); err != nil || claims.Load() != 0 || heartbeats.Load() != 2 { + t.Fatalf("startup cancellation error/heartbeats/claims = %v/%d/%d", err, heartbeats.Load(), claims.Load()) + } +} + +func TestRemoteWorkerPollsAfterTransientClaimFailure(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + var claims atomic.Int32 + plane := &recoveryRemotePlane{fakeRemotePlane: &fakeRemotePlane{}} + plane.claim = func(context.Context, string, int32) (*repositorynodepb.RepositoryTaskSpec, error) { + switch claims.Add(1) { + case 1: + return nil, errors.New("control connection is closed") + case 2: + return remoteTaskSpec(), nil + default: + cancel() + return nil, nil + } + } + executor := &recoveryRemoteExecutor{execute: func(context.Context, *repositorynodepb.RepositoryTaskSpec) (RemoteOutcome, error) { + return validRemoteOutcome(), nil + }} + worker := newRecoveryRemoteWorker(t, plane, executor) + if err := worker.Run(ctx); err != nil { + t.Fatal(err) + } + if ctx.Err() == context.DeadlineExceeded || claims.Load() != 3 || executor.executions.Load() != 1 || + plane.prepares != 1 || plane.confirms != 1 || executor.activations.Load() != 1 { + t.Fatalf("claim recovery calls/execute/prepare/confirm/activate = %d/%d/%d/%d/%d", + claims.Load(), executor.executions.Load(), plane.prepares, plane.confirms, executor.activations.Load()) + } +} + +func TestRemoteWorkerHeartbeatFailurePreventsCommit(t *testing.T) { + for _, name := range []string{"connection_closed", "ownership_lost"} { + t.Run(name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + failure := errors.New(name) + var heartbeats, canceled atomic.Int32 + plane := &recoveryRemotePlane{fakeRemotePlane: &fakeRemotePlane{}} + plane.heartbeatTask = func(_ context.Context, input controlplane.TaskHeartbeat) (*repositorynodepb.HeartbeatTaskResponse, error) { + if input.TaskID != remoteTestTaskID || input.FencingToken != 9 { + t.Errorf("heartbeat identity = %#v", input) + } + heartbeats.Add(1) + return nil, failure + } + executor := &recoveryRemoteExecutor{execute: func(ctx context.Context, _ *repositorynodepb.RepositoryTaskSpec) (RemoteOutcome, error) { + <-ctx.Done() + canceled.Add(1) + // Even an executor returning a complete result after cancellation + // must not publish it after renewal/ownership has failed. + return validRemoteOutcome(), nil + }} + worker := newRecoveryRemoteWorker(t, plane, executor) + worker.lease = 3 * time.Millisecond + err := worker.ExecuteClaimed(ctx, remoteTaskSpec()) + if !errors.Is(err, failure) || heartbeats.Load() != 1 || canceled.Load() != 1 || + plane.prepares != 0 || plane.confirms != 0 || plane.failures != 0 || executor.activations.Load() != 0 { + t.Fatalf("lost lease error/heartbeat/canceled/prepare/confirm/failure/activate = %v/%d/%d/%d/%d/%d/%d", + err, heartbeats.Load(), canceled.Load(), plane.prepares, plane.confirms, plane.failures, executor.activations.Load()) + } + }) + } +} + +func TestRemoteWorkerCancellationReportsCanceledWithoutPublishing(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + plane := &recoveryRemotePlane{fakeRemotePlane: &fakeRemotePlane{}} + plane.heartbeatTask = func(context.Context, controlplane.TaskHeartbeat) (*repositorynodepb.HeartbeatTaskResponse, error) { + return &repositorynodepb.HeartbeatTaskResponse{CancelRequested: true}, nil + } + executor := &recoveryRemoteExecutor{execute: func(ctx context.Context, _ *repositorynodepb.RepositoryTaskSpec) (RemoteOutcome, error) { + <-ctx.Done() + return validRemoteOutcome(), ctx.Err() + }} + worker := newRecoveryRemoteWorker(t, plane, executor) + worker.lease = 3 * time.Millisecond + if err := worker.ExecuteClaimed(ctx, remoteTaskSpec()); err != nil { + t.Fatal(err) + } + if plane.failures != 1 || plane.failure.Status != repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_CANCELED || + plane.failure.ErrorCode != "CANCELED_BY_USER" || plane.prepares != 0 || plane.confirms != 0 || executor.activations.Load() != 0 { + t.Fatalf("cancellation result = %#v prepare/confirm/activate=%d/%d/%d", + plane.failure, plane.prepares, plane.confirms, executor.activations.Load()) + } +} + +func TestRemoteWorkerProcessesNextTaskAfterConnectionLoss(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + var claims atomic.Int32 + next := remoteTaskSpec() + next.TaskId = "55555555-5555-4555-8555-555555555555" + next.FencingToken = 10 + plane := &recoveryRemotePlane{fakeRemotePlane: &fakeRemotePlane{}} + plane.claim = func(context.Context, string, int32) (*repositorynodepb.RepositoryTaskSpec, error) { + switch claims.Add(1) { + case 1: + return remoteTaskSpec(), nil + case 2: + return next, nil + default: + cancel() + return nil, nil + } + } + plane.heartbeatTask = func(_ context.Context, input controlplane.TaskHeartbeat) (*repositorynodepb.HeartbeatTaskResponse, error) { + if input.TaskID == remoteTestTaskID { + return nil, errors.New("control connection is closed") + } + return &repositorynodepb.HeartbeatTaskResponse{}, nil + } + executor := &recoveryRemoteExecutor{execute: func(ctx context.Context, spec *repositorynodepb.RepositoryTaskSpec) (RemoteOutcome, error) { + if spec.GetTaskId() == remoteTestTaskID { + <-ctx.Done() + } + return validRemoteOutcome(), nil + }} + worker := newRecoveryRemoteWorker(t, plane, executor) + worker.lease = 3 * time.Millisecond + if err := worker.Run(ctx); err != nil { + t.Fatal(err) + } + if ctx.Err() == context.DeadlineExceeded || executor.executions.Load() != 2 || executor.activations.Load() != 1 || + plane.prepares != 1 || plane.prepared.TaskID != next.TaskId || plane.prepared.FencingToken != next.FencingToken || + plane.confirms != 1 || plane.failures != 0 { + t.Fatalf("connection-loss recovery execute/activate/prepare/confirm/failure = %d/%d/%d/%d/%d prepared=%#v", + executor.executions.Load(), executor.activations.Load(), plane.prepares, plane.confirms, plane.failures, plane.prepared) + } +} + +func TestRemoteWorkerPrepareFailureDoesNotReplayOrActivate(t *testing.T) { + failure := errors.New("prepare response lost") + plane := &recoveryRemotePlane{fakeRemotePlane: &fakeRemotePlane{}} + plane.prepare = func(ctx context.Context, input controlplane.PreparedRevision) (*repositorynodepb.PrepareTaskCommitResponse, error) { + _, _ = plane.fakeRemotePlane.PrepareTaskCommit(ctx, input) + return nil, failure + } + executor := &recoveryRemoteExecutor{execute: func(context.Context, *repositorynodepb.RepositoryTaskSpec) (RemoteOutcome, error) { + return validRemoteOutcome(), nil + }} + worker := newRecoveryRemoteWorker(t, plane, executor) + err := worker.ExecuteClaimed(context.Background(), remoteTaskSpec()) + if !errors.Is(err, failure) || plane.prepares != 1 || plane.confirms != 0 || + plane.failures != 0 || executor.activations.Load() != 0 { + t.Fatalf("ambiguous prepare error=%v prepare/confirm/failure/activate=%d/%d/%d/%d", + err, plane.prepares, plane.confirms, plane.failures, executor.activations.Load()) + } +} + +func newRecoveryRemoteWorker(t *testing.T, plane controlplane.Plane, executor RemoteExecutor) *RemoteWorker { + t.Helper() + worker, err := NewRemoteWorker(remoteTestNodeID, "recovery-worker", plane, fakeNodeProbe{}, executor, + 60*time.Second, time.Millisecond, time.Millisecond) + if err != nil { + t.Fatal(err) + } + return worker +} + +type recoveryRemoteExecutor struct { + execute func(context.Context, *repositorynodepb.RepositoryTaskSpec) (RemoteOutcome, error) + executions atomic.Int32 + activations atomic.Int32 +} + +func (f *recoveryRemoteExecutor) Execute(ctx context.Context, spec *repositorynodepb.RepositoryTaskSpec) (RemoteOutcome, error) { + f.executions.Add(1) + return f.execute(ctx, spec) +} + +func (f *recoveryRemoteExecutor) Activate(context.Context, *repositorynodepb.RepositoryTaskSpec, RemoteOutcome) error { + f.activations.Add(1) + return nil +} + +type recoveryRemotePlane struct { + *fakeRemotePlane + heartbeatNode func(context.Context, controlplane.NodeHeartbeat) (*repositorynodepb.HeartbeatNodeResponse, error) + claim func(context.Context, string, int32) (*repositorynodepb.RepositoryTaskSpec, error) + heartbeatTask func(context.Context, controlplane.TaskHeartbeat) (*repositorynodepb.HeartbeatTaskResponse, error) + prepare func(context.Context, controlplane.PreparedRevision) (*repositorynodepb.PrepareTaskCommitResponse, error) +} + +func (f *recoveryRemotePlane) HeartbeatNode(ctx context.Context, input controlplane.NodeHeartbeat) (*repositorynodepb.HeartbeatNodeResponse, error) { + if f.heartbeatNode != nil { + return f.heartbeatNode(ctx, input) + } + return f.fakeRemotePlane.HeartbeatNode(ctx, input) +} + +func (f *recoveryRemotePlane) ClaimTask(ctx context.Context, workerID string, lease int32) (*repositorynodepb.RepositoryTaskSpec, error) { + return f.claim(ctx, workerID, lease) +} + +func (f *recoveryRemotePlane) HeartbeatTask(ctx context.Context, input controlplane.TaskHeartbeat) (*repositorynodepb.HeartbeatTaskResponse, error) { + if f.heartbeatTask != nil { + return f.heartbeatTask(ctx, input) + } + return f.fakeRemotePlane.HeartbeatTask(ctx, input) +} + +func (f *recoveryRemotePlane) PrepareTaskCommit(ctx context.Context, input controlplane.PreparedRevision) (*repositorynodepb.PrepareTaskCommitResponse, error) { + if f.prepare != nil { + return f.prepare(ctx, input) + } + return f.fakeRemotePlane.PrepareTaskCommit(ctx, input) +} + +var _ controlplane.Plane = (*recoveryRemotePlane)(nil) diff --git a/manager/backend/services/footstone/repositoryworker/remote_worker_test.go b/manager/backend/services/footstone/repositoryworker/remote_worker_test.go new file mode 100644 index 0000000000000000000000000000000000000000..f401ee8f71998922f774935e564ad2aaf92d6465 --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/remote_worker_test.go @@ -0,0 +1,229 @@ +package repositoryworker + +import ( + "context" + "errors" + "strings" + "testing" + "time" + + repositorypb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repository" + repositorynodepb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/repositorynode" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/repositoryworker/controlplane" +) + +const ( + remoteTestNodeID = "11111111-1111-4111-8111-111111111111" + remoteTestTaskID = "22222222-2222-4222-8222-222222222222" + remoteTestRepoID = "33333333-3333-4333-8333-333333333333" + remoteTestRevID = "44444444-4444-4444-8444-444444444444" +) + +func TestRemoteWorkerSuccessUsesPrepareActivateConfirm(t *testing.T) { + plane := &fakeRemotePlane{} + executor := &fakeRemoteExecutor{outcome: validRemoteOutcome()} + worker := newTestRemoteWorker(t, plane, executor) + if err := worker.ExecuteClaimed(context.Background(), remoteTaskSpec()); err != nil { + t.Fatal(err) + } + if executor.activations != 1 || plane.prepares != 1 || plane.confirms != 1 || plane.failures != 0 { + t.Fatalf("activation/prepare/confirm/failure = %d/%d/%d/%d", + executor.activations, plane.prepares, plane.confirms, plane.failures) + } + if plane.events[0] != "prepare" || plane.events[1] != "activate" || plane.events[2] != "confirm" { + t.Fatalf("commit event order = %#v", plane.events) + } +} + +func TestRemoteWorkerPreservesPartialSuccessAtPrepare(t *testing.T) { + plane := &fakeRemotePlane{} + outcome := validRemoteOutcome() + outcome.CompletionStatus = repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_PARTIAL_SUCCESS + outcome.ErrorCode = "SOME_PACKAGES_REJECTED" + outcome.ErrorMessage = "accepted=2 rejected=1" + worker := newTestRemoteWorker(t, plane, &fakeRemoteExecutor{outcome: outcome}) + if err := worker.ExecuteClaimed(context.Background(), remoteTaskSpec()); err != nil { + t.Fatal(err) + } + if plane.prepared.CompletionStatus != outcome.CompletionStatus || + plane.prepared.ErrorCode != outcome.ErrorCode || plane.prepared.ErrorMessage != outcome.ErrorMessage { + t.Fatalf("prepared partial result = %#v", plane.prepared) + } +} + +func TestRemoteWorkerFailureDoesNotActivate(t *testing.T) { + plane := &fakeRemotePlane{} + executor := &fakeRemoteExecutor{err: FailedExecution("SYNC_DOWNLOAD_FAILED", "upstream unavailable")} + worker := newTestRemoteWorker(t, plane, executor) + if err := worker.ExecuteClaimed(context.Background(), remoteTaskSpec()); err != nil { + t.Fatal(err) + } + if executor.activations != 0 || plane.prepares != 0 || plane.confirms != 0 || plane.failures != 1 { + t.Fatalf("activation/prepare/confirm/failure = %d/%d/%d/%d", + executor.activations, plane.prepares, plane.confirms, plane.failures) + } + if plane.failure.ErrorCode != "SYNC_DOWNLOAD_FAILED" || + plane.failure.Status != repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_FAILED { + t.Fatalf("failure = %#v", plane.failure) + } +} + +func TestRemoteWorkerDistributionCompletesOnControlPlaneWithoutRevisionCommit(t *testing.T) { + plane := &fakeRemotePlane{} + executor := &fakeRemoteExecutor{outcome: RemoteOutcome{ + CompletionStatus: repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_SUCCESS, + }} + worker := newTestRemoteWorker(t, plane, executor) + task := remoteTaskSpec() + task.TaskType = repositorypb.RepositoryTaskType_REPOSITORY_TASK_TYPE_DISTRIBUTE + if err := worker.ExecuteClaimed(context.Background(), task); err != nil { + t.Fatal(err) + } + if executor.activations != 0 || plane.prepares != 0 || plane.confirms != 0 || plane.failures != 0 { + t.Fatalf("distribution used revision commit path: activation/prepare/confirm/failure = %d/%d/%d/%d", + executor.activations, plane.prepares, plane.confirms, plane.failures) + } +} + +func TestRemoteWorkerFailureCarriesValidatedQuarantineResult(t *testing.T) { + plane := &fakeRemotePlane{} + executor := &fakeRemoteExecutor{outcome: RemoteOutcome{ + FailureResultObjectID: "task-logs/" + remoteTestTaskID + "/result.jsonl.zst", + FailureResultObjectSHA256: strings.Repeat("c", 64), QuarantineCount: 2, + }, err: FailedExecution("ALL_PACKAGES_REJECTED", "all packages were rejected")} + worker := newTestRemoteWorker(t, plane, executor) + if err := worker.ExecuteClaimed(context.Background(), remoteTaskSpec()); err != nil { + t.Fatal(err) + } + if plane.failure.ResultObjectID != executor.outcome.FailureResultObjectID || + plane.failure.ResultObjectSHA256 != executor.outcome.FailureResultObjectSHA256 || + plane.failure.QuarantineCount != 2 || plane.prepares != 0 || executor.activations != 0 { + t.Fatalf("reported failure result = %#v", plane.failure) + } +} + +func TestRemoteWorkerConfirmFailureLeavesPreparedForRecovery(t *testing.T) { + plane := &fakeRemotePlane{confirmErr: errors.New("control plane unavailable")} + executor := &fakeRemoteExecutor{outcome: validRemoteOutcome()} + worker := newTestRemoteWorker(t, plane, executor) + if err := worker.ExecuteClaimed(context.Background(), remoteTaskSpec()); err == nil { + t.Fatal("ExecuteClaimed() succeeded despite confirm failure") + } + if executor.activations != 1 || plane.prepares != 1 || plane.confirms != 1 || plane.failures != 0 { + t.Fatalf("prepared revision was incorrectly converted to a failure") + } +} + +func TestRemoteWorkerRejectsTaskForAnotherNode(t *testing.T) { + worker := newTestRemoteWorker(t, &fakeRemotePlane{}, &fakeRemoteExecutor{}) + task := remoteTaskSpec() + task.StorageNodeId = "55555555-5555-4555-8555-555555555555" + if err := worker.ExecuteClaimed(context.Background(), task); err == nil { + t.Fatal("ExecuteClaimed() accepted another node's task") + } +} + +func newTestRemoteWorker(t *testing.T, plane *fakeRemotePlane, executor *fakeRemoteExecutor) *RemoteWorker { + t.Helper() + plane.events = make([]string, 0, 3) + executor.events = &plane.events + worker, err := NewRemoteWorker(remoteTestNodeID, "worker-test", plane, fakeNodeProbe{}, executor, + 60*time.Second, time.Millisecond, 10*time.Second) + if err != nil { + t.Fatal(err) + } + return worker +} + +func remoteTaskSpec() *repositorynodepb.RepositoryTaskSpec { + return &repositorynodepb.RepositoryTaskSpec{TaskId: remoteTestTaskID, RepositoryId: remoteTestRepoID, + StorageNodeId: remoteTestNodeID, FencingToken: 9} +} + +func validRemoteOutcome() RemoteOutcome { + return RemoteOutcome{RevisionID: remoteTestRevID, ManifestSHA256: strings.Repeat("a", 64), + ResultObjectID: "task-results/result.jsonl.zst", ResultObjectSHA256: strings.Repeat("b", 64), + PackageCount: 3, TotalBytes: 4096, + CompletionStatus: repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_SUCCESS} +} + +type fakeNodeProbe struct{} + +func (fakeNodeProbe) Probe(context.Context) (controlplane.NodeHeartbeat, error) { + return controlplane.NodeHeartbeat{FilesystemType: "xfs", CapacityBytes: 1024, FreeBytes: 512}, nil +} + +type fakeRemoteExecutor struct { + outcome RemoteOutcome + err error + activations int + events *[]string +} + +func (f *fakeRemoteExecutor) Execute(context.Context, *repositorynodepb.RepositoryTaskSpec) (RemoteOutcome, error) { + return f.outcome, f.err +} + +func (f *fakeRemoteExecutor) Activate(context.Context, *repositorynodepb.RepositoryTaskSpec, RemoteOutcome) error { + f.activations++ + *f.events = append(*f.events, "activate") + return nil +} + +type fakeRemotePlane struct { + prepares int + confirms int + failures int + failure controlplane.TaskFailure + prepared controlplane.PreparedRevision + confirmErr error + events []string +} + +func (f *fakeRemotePlane) HeartbeatNode(context.Context, controlplane.NodeHeartbeat) ( + *repositorynodepb.HeartbeatNodeResponse, error, +) { + return &repositorynodepb.HeartbeatNodeResponse{NodeEnabled: true}, nil +} + +func (f *fakeRemotePlane) ClaimTask(context.Context, string, int32) (*repositorynodepb.RepositoryTaskSpec, error) { + return nil, nil +} + +func (f *fakeRemotePlane) HeartbeatTask(context.Context, controlplane.TaskHeartbeat) ( + *repositorynodepb.HeartbeatTaskResponse, error, +) { + return &repositorynodepb.HeartbeatTaskResponse{}, nil +} + +func (f *fakeRemotePlane) ReportTaskFailure(_ context.Context, input controlplane.TaskFailure) error { + f.failures++ + f.failure = input + return nil +} + +func (f *fakeRemotePlane) ExecuteDistributionTask(context.Context, + controlplane.DistributionExecution, +) (controlplane.DistributionResult, error) { + return controlplane.DistributionResult{ + Status: repositorypb.RepositoryTaskStatus_REPOSITORY_TASK_STATUS_SUCCESS, + }, nil +} + +func (f *fakeRemotePlane) PrepareTaskCommit(_ context.Context, input controlplane.PreparedRevision) ( + *repositorynodepb.PrepareTaskCommitResponse, error, +) { + f.prepares++ + f.prepared = input + f.events = append(f.events, "prepare") + return &repositorynodepb.PrepareTaskCommitResponse{CommitToken: string(make([]byte, 64)), + RevisionId: input.RevisionID, ManifestSha256: input.ManifestSHA256}, nil +} + +func (f *fakeRemotePlane) ConfirmTaskCommit(context.Context, controlplane.ConfirmedRevision) error { + f.confirms++ + f.events = append(f.events, "confirm") + return f.confirmErr +} + +var _ controlplane.Plane = (*fakeRemotePlane)(nil) diff --git a/manager/backend/services/footstone/repositoryworker/repository_usage.go b/manager/backend/services/footstone/repositoryworker/repository_usage.go new file mode 100644 index 0000000000000000000000000000000000000000..de971d3d2f1a44b9de0a891e58ca395099252081 --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/repository_usage.go @@ -0,0 +1,277 @@ +package repositoryworker + +import ( + "context" + "encoding/json" + "fmt" + "io" + "io/fs" + "math" + "os" + "path/filepath" + "sort" + "time" + + "github.com/google/uuid" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/repositoryworker/controlplane" +) + +type repositoryUsageAccumulator struct { + repositoryID string + logicalBytes int64 + publishStatus string + roots map[string]struct{} +} + +func (p *RepositoryNodeProbe) collectRepositoryUsages(ctx context.Context, observedAt time.Time) ( + []controlplane.RepositoryUsage, error, +) { + p.usageMu.Lock() + defer p.usageMu.Unlock() + if !p.usageScannedAt.IsZero() && observedAt.Sub(p.usageScannedAt) < p.usageScanInterval { + return cloneRepositoryUsages(p.repositoryUsages), nil + } + values, err := scanRepositoryUsages(ctx, p.store.Root(), observedAt) + if err != nil { + return nil, err + } + p.usageScannedAt = observedAt + p.repositoryUsages = cloneRepositoryUsages(values) + return values, nil +} + +func cloneRepositoryUsages(values []controlplane.RepositoryUsage) []controlplane.RepositoryUsage { + if len(values) == 0 { + return nil + } + result := make([]controlplane.RepositoryUsage, len(values)) + copy(result, values) + return result +} + +func scanRepositoryUsages(ctx context.Context, root string, observedAt time.Time) ( + []controlplane.RepositoryUsage, error, +) { + accumulators := make(map[string]*repositoryUsageAccumulator) + for _, directory := range []string{"revisions-private", "quarantine", "keys"} { + if err := discoverRepositoryDirectories(filepath.Join(root, directory), accumulators); err != nil { + return nil, fmt.Errorf("scan %s repository usage: %w", directory, err) + } + } + if err := discoverPublishedRepositories(filepath.Join(root, "public"), accumulators); err != nil { + return nil, fmt.Errorf("scan published repository usage: %w", err) + } + for _, directory := range []string{"staging", "trash"} { + if err := discoverManifestOwnedRoots(ctx, filepath.Join(root, directory), accumulators); err != nil { + return nil, fmt.Errorf("scan %s repository usage: %w", directory, err) + } + } + + ids := make([]string, 0, len(accumulators)) + for id := range accumulators { + ids = append(ids, id) + } + sort.Strings(ids) + result := make([]controlplane.RepositoryUsage, 0, len(ids)) + for _, id := range ids { + if err := ctx.Err(); err != nil { + return nil, err + } + value := accumulators[id] + paths := make([]string, 0, len(value.roots)) + for path := range value.roots { + paths = append(paths, path) + } + sort.Strings(paths) + physicalBytes, err := allocatedTreeBytes(ctx, paths) + if err != nil { + return nil, fmt.Errorf("measure repository %s physical usage: %w", id, err) + } + result = append(result, controlplane.RepositoryUsage{ + RepositoryID: id, LogicalBytes: value.logicalBytes, PhysicalBytes: physicalBytes, + PublishStatus: value.publishStatus, ObservedAt: observedAt, + }) + } + return result, nil +} + +func discoverRepositoryDirectories(root string, values map[string]*repositoryUsageAccumulator) error { + entries, err := os.ReadDir(root) + if os.IsNotExist(err) { + return nil + } + if err != nil { + return err + } + for _, entry := range entries { + if !entry.IsDir() || uuid.Validate(entry.Name()) != nil { + continue + } + usage := ensureRepositoryUsage(values, entry.Name()) + usage.roots[filepath.Join(root, entry.Name())] = struct{}{} + } + return nil +} + +func discoverPublishedRepositories(root string, values map[string]*repositoryUsageAccumulator) error { + entries, err := os.ReadDir(root) + if os.IsNotExist(err) { + return nil + } + if err != nil { + return err + } + for _, entry := range entries { + if !entry.IsDir() { + continue + } + publishRoot := filepath.Join(root, entry.Name()) + manifest, manifestErr := readPublishedManifest(filepath.Join(publishRoot, "current", "manifest.json")) + if os.IsNotExist(manifestErr) { + manifest, manifestErr = firstPublishedManifest(filepath.Join(publishRoot, "revisions")) + } + if manifestErr != nil { + if os.IsNotExist(manifestErr) { + continue + } + return manifestErr + } + usage := ensureRepositoryUsage(values, manifest.RepositoryID) + usage.roots[publishRoot] = struct{}{} + if currentManifest, currentErr := readPublishedManifest(filepath.Join(publishRoot, "current", "manifest.json")); currentErr == nil { + logicalBytes, sumErr := manifestLogicalBytes(currentManifest) + if sumErr != nil { + return sumErr + } + usage.logicalBytes = logicalBytes + usage.publishStatus = "READY" + } + } + return nil +} + +func firstPublishedManifest(root string) (publishedManifest, error) { + entries, err := os.ReadDir(root) + if err != nil { + return publishedManifest{}, err + } + for _, entry := range entries { + if !entry.IsDir() { + continue + } + value, readErr := readPublishedManifest(filepath.Join(root, entry.Name(), "manifest.json")) + if readErr == nil { + return value, nil + } + if !os.IsNotExist(readErr) { + return publishedManifest{}, readErr + } + } + return publishedManifest{}, os.ErrNotExist +} + +func discoverManifestOwnedRoots(ctx context.Context, root string, + values map[string]*repositoryUsageAccumulator, +) error { + return filepath.WalkDir(root, func(path string, entry fs.DirEntry, walkErr error) error { + if walkErr != nil { + if os.IsNotExist(walkErr) { + return nil + } + return walkErr + } + if err := ctx.Err(); err != nil { + return err + } + if entry.IsDir() || entry.Name() != "manifest.json" { + return nil + } + manifest, err := readPublishedManifest(path) + if err != nil { + return err + } + usage := ensureRepositoryUsage(values, manifest.RepositoryID) + usage.roots[filepath.Dir(path)] = struct{}{} + return nil + }) +} + +func ensureRepositoryUsage(values map[string]*repositoryUsageAccumulator, + repositoryID string, +) *repositoryUsageAccumulator { + if value := values[repositoryID]; value != nil { + return value + } + value := &repositoryUsageAccumulator{repositoryID: repositoryID, + publishStatus: "UNPUBLISHED", roots: make(map[string]struct{})} + values[repositoryID] = value + return value +} + +func readPublishedManifest(path string) (publishedManifest, error) { + file, err := os.Open(path) + if err != nil { + return publishedManifest{}, err + } + defer file.Close() + info, err := file.Stat() + if err != nil { + return publishedManifest{}, err + } + if !info.Mode().IsRegular() || info.Size() <= 0 || info.Size() > maxPublishedManifestBytes { + return publishedManifest{}, fmt.Errorf("published manifest %s has invalid size", path) + } + decoder := json.NewDecoder(io.LimitReader(file, maxPublishedManifestBytes+1)) + var value publishedManifest + if err = decoder.Decode(&value); err != nil { + return publishedManifest{}, fmt.Errorf("decode published manifest %s: %w", path, err) + } + if uuid.Validate(value.RepositoryID) != nil || uuid.Validate(value.RevisionID) != nil { + return publishedManifest{}, fmt.Errorf("published manifest %s has invalid identity", path) + } + return value, nil +} + +func manifestLogicalBytes(value publishedManifest) (int64, error) { + var result int64 + for _, item := range value.Packages { + if item.SizeBytes < 0 || result > math.MaxInt64-item.SizeBytes { + return 0, fmt.Errorf("repository %s manifest package size overflows", value.RepositoryID) + } + result += item.SizeBytes + } + return result, nil +} + +func allocatedTreeBytes(ctx context.Context, roots []string) (int64, error) { + seen := make(map[string]struct{}) + var total int64 + for _, root := range roots { + err := filepath.Walk(root, func(path string, info fs.FileInfo, walkErr error) error { + if walkErr != nil { + if os.IsNotExist(walkErr) { + return nil + } + return walkErr + } + if err := ctx.Err(); err != nil { + return err + } + identity, bytes := allocatedEntry(path, info) + if _, exists := seen[identity]; exists { + return nil + } + seen[identity] = struct{}{} + if bytes < 0 || total > math.MaxInt64-bytes { + return fmt.Errorf("allocated byte count overflows") + } + total += bytes + return nil + }) + if err != nil { + return 0, err + } + } + return total, nil +} diff --git a/manager/backend/services/footstone/repositoryworker/repository_usage_linux.go b/manager/backend/services/footstone/repositoryworker/repository_usage_linux.go new file mode 100644 index 0000000000000000000000000000000000000000..f0c0e208e71ed0af392cc87d45775ea14de1baf4 --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/repository_usage_linux.go @@ -0,0 +1,16 @@ +//go:build linux + +package repositoryworker + +import ( + "fmt" + "io/fs" + "syscall" +) + +func allocatedEntry(path string, info fs.FileInfo) (string, int64) { + if stat, ok := info.Sys().(*syscall.Stat_t); ok { + return fmt.Sprintf("%d:%d", stat.Dev, stat.Ino), stat.Blocks * 512 + } + return path, info.Size() +} diff --git a/manager/backend/services/footstone/repositoryworker/repository_usage_other.go b/manager/backend/services/footstone/repositoryworker/repository_usage_other.go new file mode 100644 index 0000000000000000000000000000000000000000..bfb612fbea491388eb7eaa5ff1ca215a5feb850f --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/repository_usage_other.go @@ -0,0 +1,9 @@ +//go:build !linux + +package repositoryworker + +import "io/fs" + +func allocatedEntry(path string, info fs.FileInfo) (string, int64) { + return path, info.Size() +} diff --git a/manager/backend/services/footstone/repositoryworker/repository_usage_test.go b/manager/backend/services/footstone/repositoryworker/repository_usage_test.go new file mode 100644 index 0000000000000000000000000000000000000000..4ec851aa814b23cd03d13d6a7647d10a103245bd --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/repository_usage_test.go @@ -0,0 +1,123 @@ +//go:build linux + +package repositoryworker + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "testing" + "time" + + "github.com/google/uuid" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" +) + +func TestRepositoryUsageReportsLogicalAndPhysicalBytes(t *testing.T) { + root := t.TempDir() + store, err := storage.New(config.RepositoryConfig{Root: root, MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + repositoryID := uuid.NewString() + revisionID := uuid.NewString() + publishKey := "usage-test" + privateRoot := filepath.Join(root, "revisions-private", repositoryID, revisionID) + publicRoot := filepath.Join(root, "public", publishKey, "revisions", revisionID) + for _, path := range []string{privateRoot, publicRoot, filepath.Join(root, "quarantine", repositoryID)} { + if err = os.MkdirAll(path, 0750); err != nil { + t.Fatal(err) + } + } + privateRPM := filepath.Join(privateRoot, "fixture.rpm") + if err = os.WriteFile(privateRPM, make([]byte, 8192), 0440); err != nil { + t.Fatal(err) + } + if err = os.Link(privateRPM, filepath.Join(publicRoot, "fixture.rpm")); err != nil { + t.Fatal(err) + } + manifestBytes, err := json.Marshal(publishedManifest{SchemaVersion: 3, + RepositoryID: repositoryID, RevisionID: revisionID, + Packages: []manifestPackage{{Name: "fixture", SizeBytes: 8192}}}) + if err != nil { + t.Fatal(err) + } + if err = os.WriteFile(filepath.Join(publicRoot, "manifest.json"), manifestBytes, 0444); err != nil { + t.Fatal(err) + } + if err = os.Symlink(filepath.ToSlash(filepath.Join("revisions", revisionID)), + filepath.Join(root, "public", publishKey, "current")); err != nil { + t.Fatal(err) + } + if err = os.WriteFile(filepath.Join(root, "quarantine", repositoryID, "rejected.rpm"), + make([]byte, 4096), 0440); err != nil { + t.Fatal(err) + } + + observedAt := time.Date(2026, 9, 17, 1, 2, 3, 0, time.UTC) + values, err := scanRepositoryUsages(context.Background(), store.Root(), observedAt) + if err != nil { + t.Fatal(err) + } + if len(values) != 1 || values[0].RepositoryID != repositoryID || values[0].LogicalBytes != 8192 || + values[0].PhysicalBytes <= 0 || values[0].PublishStatus != "READY" || + !values[0].ObservedAt.Equal(observedAt) { + t.Fatalf("repository usages = %#v", values) + } + + privateBytes, err := allocatedTreeBytes(context.Background(), []string{privateRoot}) + if err != nil { + t.Fatal(err) + } + publicBytes, err := allocatedTreeBytes(context.Background(), []string{publicRoot}) + if err != nil { + t.Fatal(err) + } + combinedBytes, err := allocatedTreeBytes(context.Background(), []string{privateRoot, publicRoot}) + if err != nil { + t.Fatal(err) + } + if combinedBytes >= privateBytes+publicBytes { + t.Fatalf("hard-linked RPM was counted twice: private=%d public=%d combined=%d", + privateBytes, publicBytes, combinedBytes) + } +} + +func TestRepositoryUsageProbeCachesFilesystemScan(t *testing.T) { + root := t.TempDir() + store, err := storage.New(config.RepositoryConfig{Root: root, MinimumFreeBytes: 1}) + if err != nil { + t.Fatal(err) + } + repositoryID := uuid.NewString() + path := filepath.Join(root, "quarantine", repositoryID, "fixture.rpm") + if err = os.MkdirAll(filepath.Dir(path), 0750); err != nil { + t.Fatal(err) + } + if err = os.WriteFile(path, []byte("fixture"), 0440); err != nil { + t.Fatal(err) + } + probe, err := NewRepositoryNodeProbe(store) + if err != nil { + t.Fatal(err) + } + now := time.Now().UTC() + first, err := probe.collectRepositoryUsages(context.Background(), now) + if err != nil || len(first) != 1 { + t.Fatalf("first scan=%#v err=%v", first, err) + } + if err = os.RemoveAll(filepath.Join(root, "quarantine", repositoryID)); err != nil { + t.Fatal(err) + } + cached, err := probe.collectRepositoryUsages(context.Background(), now.Add(30*time.Second)) + if err != nil || len(cached) != 1 { + t.Fatalf("cached scan=%#v err=%v", cached, err) + } + refreshed, err := probe.collectRepositoryUsages(context.Background(), now.Add(2*time.Minute)) + if err != nil || len(refreshed) != 0 { + t.Fatalf("refreshed scan=%#v err=%v", refreshed, err) + } +} diff --git a/manager/backend/services/footstone/repositoryworker/runner/main.go b/manager/backend/services/footstone/repositoryworker/runner/main.go new file mode 100644 index 0000000000000000000000000000000000000000..fb369e1fa6f3e7d386f1a188c8fd8903d4e85142 --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/runner/main.go @@ -0,0 +1,117 @@ +package runner + +import ( + "context" + "flag" + "fmt" + "os" + "os/exec" + "os/signal" + "syscall" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryTaskService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/repositoryworker" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/repositoryworker/bootstrap" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/repositoryworker/controlplane" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/repositoryworker/nodeapi" +) + +func Main() error { + var appConfigPath, workerID string + var leaseSeconds, pollMilliseconds int + flag.StringVar(&appConfigPath, "appconf", "./application.yaml", "application config file") + flag.StringVar(&workerID, "worker-id", "", "stable repository worker identifier") + flag.IntVar(&leaseSeconds, "lease-seconds", 60, "repository task lease duration") + flag.IntVar(&pollMilliseconds, "poll-milliseconds", 1000, "empty queue poll interval") + flag.Parse() + + global, err := config.LoadGlobalConfigurationFromLocalfile(appConfigPath) + if err != nil { + return err + } + appConfig, err := global.LoadApplication() + if err != nil { + return err + } + for _, tool := range []string{"rpm", "rpmkeys", "dnf", "createrepo_c"} { + if _, err = exec.LookPath(tool); err != nil { + return fmt.Errorf("repository worker requires %s: %w", tool, err) + } + } + store, err := storage.New(appConfig.Application.Repository) + if err != nil { + return err + } + if workerID == "" { + hostname, _ := os.Hostname() + workerID = fmt.Sprintf("repository-worker:%s:%d", hostname, os.Getpid()) + } + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + nodeConfig := appConfig.Application.Repository.WithDefaults().NodeClient + if nodeConfig.ControlTarget != "" { + nodeConfig, err = bootstrap.EnsureCredentials(ctx, nodeConfig, store.Root()) + if err != nil { + return err + } + control, clientErr := controlplane.New(nodeConfig, store.Root()) + if clientErr != nil { + return clientErr + } + probe, probeErr := repositoryworker.NewRepositoryNodeProbe(store) + if probeErr != nil { + return probeErr + } + executor, executorErr := repositoryworker.NewRemoteExecutor(store, control, workerID) + if executorErr != nil { + return executorErr + } + loggingExecutor, loggingErr := repositoryworker.NewLoggingRemoteExecutor(store, executor) + if loggingErr != nil { + return loggingErr + } + worker, workerErr := repositoryworker.NewRemoteWorker(nodeConfig.StorageNodeID, workerID, + control, probe, loggingExecutor, time.Duration(leaseSeconds)*time.Second, + time.Duration(pollMilliseconds)*time.Millisecond, + time.Duration(nodeConfig.HeartbeatSeconds)*time.Second) + if workerErr != nil { + return workerErr + } + nodeServer, serverErr := nodeapi.New(appConfig.Application.Repository.WithDefaults().NodeServer, store, probe) + if serverErr != nil { + return serverErr + } + return runRemoteNode(ctx, worker.Run, nodeServer.Run) + } + db, err := database.NewDBClient(appConfig.Resource.MetaDB) + if err != nil { + return err + } + defer db.Close() + tasks := repositoryTaskService.New(db) + worker, err := repositoryworker.NewWorker(workerID, tasks, repositoryworker.NewHandler(db, store), + time.Duration(leaseSeconds)*time.Second, time.Duration(pollMilliseconds)*time.Millisecond) + if err != nil { + return err + } + return worker.Run(ctx) +} + +func runRemoteNode(parent context.Context, worker, dataServer func(context.Context) error) error { + ctx, cancel := context.WithCancel(parent) + defer cancel() + results := make(chan error, 2) + go func() { results <- worker(ctx) }() + go func() { results <- dataServer(ctx) }() + first := <-results + cancel() + second := <-results + if first != nil { + return first + } + return second +} diff --git a/manager/backend/services/footstone/repositoryworker/runner/main_test.go b/manager/backend/services/footstone/repositoryworker/runner/main_test.go new file mode 100644 index 0000000000000000000000000000000000000000..473994ee39fd39d9c1593c979e501a399af6101b --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/runner/main_test.go @@ -0,0 +1,27 @@ +package runner + +import ( + "context" + "errors" + "testing" + "time" +) + +func TestRunRemoteNodeCancelsPeerOnFailure(t *testing.T) { + wanted := errors.New("data server failed") + peerStopped := make(chan struct{}) + worker := func(ctx context.Context) error { + <-ctx.Done() + close(peerStopped) + return nil + } + server := func(context.Context) error { return wanted } + if err := runRemoteNode(context.Background(), worker, server); !errors.Is(err, wanted) { + t.Fatalf("runRemoteNode() error = %v", err) + } + select { + case <-peerStopped: + case <-time.After(time.Second): + t.Fatal("remote worker was not canceled") + } +} diff --git a/manager/backend/services/footstone/repositoryworker/sync.go b/manager/backend/services/footstone/repositoryworker/sync.go new file mode 100644 index 0000000000000000000000000000000000000000..b2dec74836af1b0719b85e1561198b88df6f31ed --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/sync.go @@ -0,0 +1,477 @@ +package repositoryworker + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "sort" + "strings" + + "github.com/google/uuid" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/tasklog" +) + +type SyncTool interface { + Download(context.Context, request.RepositorySyncSnapshot, string) ([]string, error) +} + +type commandSyncTool struct{} + +func (commandSyncTool) Download(ctx context.Context, snapshot request.RepositorySyncSnapshot, + root string, +) ([]string, error) { + reposDir := filepath.Join(root, "repos.d") + cacheDir := filepath.Join(root, "cache") + downloadDir := filepath.Join(root, "packages") + for _, path := range []string{reposDir, cacheDir, downloadDir} { + if err := os.MkdirAll(path, 0700); err != nil { + return nil, err + } + } + configPath := filepath.Join(root, "dnf.conf") + configValue := syncDNFConfig(snapshot, reposDir, cacheDir) + if err := os.WriteFile(configPath, []byte(configValue), 0600); err != nil { + return nil, err + } + architectures := []string{snapshot.Architecture} + if snapshot.Architecture != request.RepositoryArchitectureNoarch { + architectures = append(architectures, request.RepositoryArchitectureNoarch) + } + args := syncDownloadArgs(snapshot, configPath, downloadDir, architectures) + command := exec.CommandContext(ctx, "dnf", args...) + command.Env = append(os.Environ(), "LC_ALL=C", "LANG=C") + output, err := runLoggedCommand(ctx, "dnf-sync", command) + unmatched := unmatchedPackageNames(output) + if len(unmatched) > 0 { + _ = tasklog.Event(ctx, "WARN", "dnf-sync", syncFilterSummary(unmatched), + map[string]string{"unmatched_packages": strings.Join(unmatched, ",")}) + } + if err != nil { + return unmatched, fmt.Errorf("dnf repository download failed: %s", truncateOutput(output)) + } + return unmatched, nil +} + +func syncDNFConfig(snapshot request.RepositorySyncSnapshot, reposDir, cacheDir string) string { + return fmt.Sprintf(`[main] +reposdir=%s +cachedir=%s +logdir=%s +keepcache=0 +strict=0 +throttle=%d +max_parallel_downloads=%d + +[ocm-upstream] +name=OCManager isolated upstream +baseurl=%s +enabled=1 +gpgcheck=0 +repo_gpgcheck=0 +`, reposDir, cacheDir, cacheDir, snapshot.BandwidthLimitBytes, snapshot.MaxParallelDownloads, snapshot.UpstreamURL) +} + +func unmatchedPackageNames(output []byte) []string { + names := make([]string, 0) + seen := make(map[string]struct{}) + for _, line := range strings.Split(string(output), "\n") { + line = strings.TrimSpace(line) + line = strings.TrimPrefix(line, "stdout: ") + line = strings.TrimPrefix(line, "stderr: ") + if !strings.HasPrefix(line, "No package ") || !strings.HasSuffix(line, " available.") { + continue + } + name := strings.TrimSuffix(strings.TrimPrefix(line, "No package "), " available.") + if name == "" { + continue + } + if _, exists := seen[name]; exists { + continue + } + seen[name] = struct{}{} + names = append(names, name) + } + sort.Strings(names) + return names +} + +func syncFilterSummary(unmatched []string) string { + if len(unmatched) == 0 { + return "no RPM packages matched the repository filters" + } + const maximum = 20 + listed, suffix := unmatched, "" + if len(listed) > maximum { + suffix = fmt.Sprintf(" (+%d more)", len(unmatched)-maximum) + listed = listed[:maximum] + } + return fmt.Sprintf("filters with no upstream match: %s%s", strings.Join(listed, ", "), suffix) +} + +func syncDownloadArgs(snapshot request.RepositorySyncSnapshot, configPath, downloadDir string, + architectures []string, +) []string { + base := []string{"-q", "-c", configPath, "--forcearch=" + snapshot.Architecture} + if len(snapshot.IncludePackages) == 0 { + args := append(base, "reposync", "--repoid=ocm-upstream", "--download-path="+downloadDir, + "--download-metadata", "--delete", "--norepopath") + for _, architecture := range architectures { + args = append(args, "--arch="+architecture) + } + return args + } + + // Filtered synchronization must constrain the network download itself. Keeping the + // post-download RPM inspection below provides a second policy check before publish. + args := append(base, "download", "--repoid=ocm-upstream", "--destdir="+downloadDir) + for _, architecture := range architectures { + args = append(args, "--arch="+architecture) + } + for _, pattern := range snapshot.ExcludePackages { + args = append(args, "--exclude="+pattern) + } + args = append(args, "--") + return append(args, snapshot.IncludePackages...) +} + +func (h *Handler) handleSync(ctx context.Context, task *po.RepositoryTask) Outcome { + var snapshot request.RepositorySyncSnapshot + if err := json.Unmarshal([]byte(task.PayloadJSON), &snapshot); err != nil { + return failed("INVALID_SYNC_PAYLOAD", "sync task payload is invalid") + } + normalizeSyncSnapshot(&snapshot) + repository, err := h.repositories.GetByID(task.RepositoryID, nil) + if err != nil { + return failed("REPOSITORY_NOT_FOUND", err.Error()) + } + if err = validateSyncSnapshot(repository, snapshot); err != nil { + return failed("INVALID_SYNC_PAYLOAD", err.Error()) + } + verifier, err := h.newSignatureSession(ctx, task, repository) + if err != nil { + return failed("GPG_KEY_UNAVAILABLE", err.Error()) + } + if verifier != nil { + defer verifier.Close() + } + taskRelative := filepath.ToSlash(filepath.Join("staging", "sync", task.ID)) + taskRoot, err := h.store.Resolve(taskRelative) + if err != nil { + return failed("SYNC_STAGING_FAILED", err.Error()) + } + if err = os.RemoveAll(taskRoot); err != nil { + return failed("SYNC_STAGING_FAILED", err.Error()) + } + defer os.RemoveAll(taskRoot) + syncBytes, _ := syncLimits(h.store.Config(), snapshot) + if err = h.store.EnsureFreeSpace(syncBytes + h.store.Config().MetadataReserve); err != nil { + return failed("SYNC_INSUFFICIENT_SPACE", err.Error()) + } + if err = os.MkdirAll(taskRoot, 0700); err != nil { + return failed("SYNC_STAGING_FAILED", err.Error()) + } + unmatched, err := h.syncer.Download(ctx, snapshot, taskRoot) + if err != nil { + return failed("SYNC_DOWNLOAD_FAILED", err.Error()) + } + candidates, err := h.inspectSyncedPackages(ctx, repository, snapshot, filepath.Join(taskRoot, "packages"), verifier) + if err != nil { + var signatureErr *signatureVerificationError + if errors.As(err, &signatureErr) { + return failed("SYNC_GPG_VERIFICATION_FAILED", signatureErr.Error()) + } + return failed("SYNC_VALIDATION_FAILED", err.Error()) + } + if len(candidates) == 0 { + return failed("SYNC_EMPTY_RESULT", syncFilterSummary(unmatched)) + } + parent, err := h.activeRevision(repository) + if err != nil { + return failed("REVISION_LOOKUP_FAILED", err.Error()) + } + if snapshot.SyncMode == request.RepositorySyncModeAppend && parent != nil { + candidates, err = h.mergeActiveRevision(ctx, repository, parent.ID, candidates) + if err != nil { + return failed("REVISION_BUILD_FAILED", err.Error()) + } + } + revision, packages, privateRelative, err := h.materializeSyncRevision(task, repository, parent, candidates) + if err != nil { + return failed("REVISION_BUILD_FAILED", err.Error()) + } + if _, err = h.publishRevision(ctx, task, repository, revision, packages, nil); err != nil { + h.moveRevisionToTrash(privateRelative, revision.ID) + return failed("REVISION_PUBLISH_FAILED", err.Error()) + } + return Outcome{Status: request.RepositoryTaskStatusSuccess, RevisionID: revision.ID} +} + +func validateSyncSnapshot(repository *po.SoftwareRepository, snapshot request.RepositorySyncSnapshot) error { + if strings.TrimSpace(snapshot.UpstreamURL) == "" || snapshot.Architecture == "" { + return fmt.Errorf("sync snapshot requires upstream_url and architecture") + } + if snapshot.Architecture != repository.Architecture { + return fmt.Errorf("sync snapshot architecture no longer matches the repository") + } + if snapshot.SyncMode != request.RepositorySyncModeMirror && snapshot.SyncMode != request.RepositorySyncModeAppend { + return fmt.Errorf("sync_mode must be MIRROR or APPEND") + } + if snapshot.BandwidthLimitBytes < 0 || snapshot.MaxParallelDownloads < 1 || + snapshot.MaxParallelDownloads > 20 { + return fmt.Errorf("sync bandwidth or parallel download setting is invalid") + } + for _, patterns := range [][]string{snapshot.IncludePackages, snapshot.ExcludePackages} { + for _, pattern := range patterns { + if pattern == "" { + return fmt.Errorf("package filter cannot be empty") + } + if _, err := filepath.Match(pattern, "probe"); err != nil { + return fmt.Errorf("invalid package filter %q: %w", pattern, err) + } + } + } + return nil +} + +func normalizeSyncSnapshot(snapshot *request.RepositorySyncSnapshot) { + if snapshot.SyncMode == "" { + snapshot.SyncMode = request.RepositorySyncModeMirror + } + if snapshot.MaxParallelDownloads == 0 { + snapshot.MaxParallelDownloads = 3 + } +} + +func (h *Handler) mergeActiveRevision(ctx context.Context, repository *po.SoftwareRepository, + revisionID string, downloaded []candidate, +) ([]candidate, error) { + relative := filepath.ToSlash(filepath.Join("revisions-private", repository.ID, revisionID)) + root, err := h.store.Resolve(relative) + if err != nil { + return nil, err + } + merged := make(map[string]candidate, len(downloaded)) + err = filepath.Walk(root, func(path string, info os.FileInfo, walkErr error) error { + if walkErr != nil { + return walkErr + } + if info.IsDir() || !strings.HasSuffix(strings.ToLower(info.Name()), ".rpm") { + return nil + } + if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 { + return fmt.Errorf("active revision contains a non-regular RPM %q", info.Name()) + } + digest, size, digestErr := fileSHA256(path) + if digestErr != nil { + return digestErr + } + infoValue, inspectErr := h.rpm.Inspect(ctx, path) + if inspectErr != nil { + return fmt.Errorf("inspect active revision RPM %q: %w", info.Name(), inspectErr) + } + if !architectureAllowed(repository.Architecture, infoValue.Architecture) { + return fmt.Errorf("active revision RPM %q has incompatible architecture %s", + info.Name(), infoValue.Architecture) + } + storedRelative, relativeErr := filepath.Rel(h.store.Root(), path) + if relativeErr != nil || strings.HasPrefix(storedRelative, "..") { + return fmt.Errorf("active revision RPM escaped repository storage") + } + nevra := formatNEVRA(infoValue) + merged[nevra] = candidate{OriginalName: storedPackageOriginalName(info.Name()), + RelativePath: filepath.ToSlash(storedRelative), Info: infoValue, SHA256: digest, + Size: size, NEVRA: nevra} + return nil + }) + if err != nil { + return nil, err + } + for _, item := range downloaded { + merged[item.NEVRA] = item + } + values := make([]candidate, 0, len(merged)) + for _, item := range merged { + values = append(values, item) + } + sort.Slice(values, func(i, j int) bool { return values[i].NEVRA < values[j].NEVRA }) + return values, nil +} + +func storedPackageOriginalName(name string) string { + if len(name) > 17 && name[16] == '-' { + return name[17:] + } + return name +} + +func (h *Handler) inspectSyncedPackages(ctx context.Context, repository *po.SoftwareRepository, + snapshot request.RepositorySyncSnapshot, root string, verifier *signatureSession, +) ([]candidate, error) { + values := make([]candidate, 0) + seen := make(map[string]string) + var downloadedBytes int64 + var downloadedPackages int + maxBytes, maxPackages := syncLimits(h.store.Config(), snapshot) + err := filepath.Walk(root, func(path string, info os.FileInfo, walkErr error) error { + if walkErr != nil { + return walkErr + } + if info.IsDir() || !strings.HasSuffix(strings.ToLower(info.Name()), ".rpm") { + return nil + } + if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 { + return fmt.Errorf("sync output contains a non-regular RPM %q", info.Name()) + } + downloadedPackages++ + downloadedBytes += info.Size() + if downloadedPackages > maxPackages || downloadedBytes > maxBytes { + return fmt.Errorf("sync result exceeds configured package or byte limit") + } + digest, size, err := fileSHA256(path) + if err != nil { + return err + } + infoValue, err := h.rpm.Inspect(ctx, path) + if err != nil { + return fmt.Errorf("validate synced RPM %q: %w", info.Name(), err) + } + item := candidate{OriginalName: info.Name(), RelativePath: "", Info: infoValue, + SHA256: digest, Size: size} + if verifyErr := evaluateCandidateSignature(ctx, verifier, path, &item); verifyErr != nil { + return fmt.Errorf("verify synced RPM %q: %w", info.Name(), verifyErr) + } + if !architectureAllowed(repository.Architecture, infoValue.Architecture) { + return fmt.Errorf("package %s architecture %s is not allowed in %s repository", + info.Name(), infoValue.Architecture, repository.Architecture) + } + if !packageMatchesFilters(infoValue.Name, snapshot.IncludePackages, snapshot.ExcludePackages) { + return nil + } + nevra := formatNEVRA(infoValue) + if prior, exists := seen[nevra]; exists { + if prior != digest { + return fmt.Errorf("NEVRA %s exists with multiple SHA-256 values", nevra) + } + return nil + } + seen[nevra] = digest + relative, err := filepath.Rel(h.store.Root(), path) + if err != nil || strings.HasPrefix(relative, "..") { + return fmt.Errorf("synced RPM escaped task staging") + } + item.RelativePath = filepath.ToSlash(relative) + item.NEVRA = nevra + values = append(values, item) + return nil + }) + if err != nil { + return nil, err + } + sort.Slice(values, func(i, j int) bool { return values[i].NEVRA < values[j].NEVRA }) + return values, nil +} + +func syncLimits(configValue config.RepositoryConfig, snapshot request.RepositorySyncSnapshot) (int64, int) { + return configValue.SyncLimits(len(snapshot.IncludePackages) > 0) +} + +func packageMatchesFilters(name string, include, exclude []string) bool { + included := len(include) == 0 + for _, pattern := range include { + if matched, _ := filepath.Match(pattern, name); matched { + included = true + break + } + } + if !included { + return false + } + for _, pattern := range exclude { + if matched, _ := filepath.Match(pattern, name); matched { + return false + } + } + return true +} + +func (h *Handler) materializeSyncRevision(task *po.RepositoryTask, repository *po.SoftwareRepository, + parent *po.RepositoryRevision, candidates []candidate, +) (*po.RepositoryRevision, []po.RepositoryPackage, string, error) { + revisionID := uuid.NewString() + stagingRelative := filepath.ToSlash(filepath.Join("staging", "revisions", task.ID, revisionID)) + privateRelative := filepath.ToSlash(filepath.Join("revisions-private", repository.ID, revisionID)) + staging, err := h.store.Resolve(stagingRelative) + if err != nil { + return nil, nil, "", err + } + privateRoot, err := h.store.Resolve(privateRelative) + if err != nil { + return nil, nil, "", err + } + if err = os.RemoveAll(filepath.Dir(staging)); err != nil { + return nil, nil, "", err + } + if err = os.MkdirAll(staging, 0750); err != nil { + return nil, nil, "", err + } + packages := make([]po.RepositoryPackage, 0, len(candidates)) + for _, item := range candidates { + source, resolveErr := h.store.Resolve(item.RelativePath) + if resolveErr != nil { + return nil, nil, "", resolveErr + } + name := safePackageFileName(item.SHA256, item.OriginalName) + if err = linkOrCopy(source, filepath.Join(staging, name)); err != nil { + return nil, nil, "", err + } + packages = append(packages, po.RepositoryPackage{ + ID: uuid.NewString(), RepositoryID: repository.ID, RevisionID: revisionID, + Name: item.Info.Name, Epoch: normalizeEpoch(item.Info.Epoch), Version: item.Info.Version, + Release: item.Info.Release, Architecture: item.Info.Architecture, NEVRA: item.NEVRA, + FileName: item.OriginalName, RelativePath: filepath.ToSlash(filepath.Join(privateRelative, name)), + SizeBytes: item.Size, SHA256: item.SHA256, Source: request.RepositoryPackageSourceSync, + SignatureStatus: item.SignatureStatus, SignerKeyID: item.SignerKeyID, + SignatureMessage: item.SignatureMessage, + }) + } + if err = os.MkdirAll(filepath.Dir(privateRoot), 0750); err != nil { + return nil, nil, "", err + } + if err = os.Rename(staging, privateRoot); err != nil { + return nil, nil, "", fmt.Errorf("finalize sync revision: %w", err) + } + for _, pkg := range packages { + path, resolveErr := h.store.Resolve(pkg.RelativePath) + if resolveErr == nil { + _ = os.Chmod(path, 0440) + } + } + manifest, total := packageManifest(packages) + parentID := "" + if parent != nil { + parentID = parent.ID + } + revision := &po.RepositoryRevision{ + ID: revisionID, RepositoryID: repository.ID, StorageNodeID: task.StorageNodeID, + ParentRevisionID: parentID, TaskID: task.ID, + Kind: request.RepositoryRevisionKindSync, Status: request.RepositoryRevisionStatusReady, + PackageCount: len(packages), TotalBytes: total, ManifestSHA256: manifest, + RelativePath: privateRelative, CreatedBy: task.RequestedBy, + } + return revision, packages, privateRelative, nil +} + +func (h *Handler) activeRevision(repository *po.SoftwareRepository) (*po.RepositoryRevision, error) { + if repository.ActiveRevisionID != "" { + return h.content.GetRevision(repository.ID, repository.ActiveRevisionID, nil) + } + return h.content.LatestReadyRevision(repository.ID, nil) +} diff --git a/manager/backend/services/footstone/repositoryworker/sync_command_test.go b/manager/backend/services/footstone/repositoryworker/sync_command_test.go new file mode 100644 index 0000000000000000000000000000000000000000..673f503fd11d8ed0632946b2cff3794bd5c2112b --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/sync_command_test.go @@ -0,0 +1,103 @@ +package repositoryworker + +import ( + "reflect" + "strings" + "testing" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" +) + +func TestSyncDownloadArgsUsesRepoSyncForUnfilteredSnapshot(t *testing.T) { + snapshot := request.RepositorySyncSnapshot{Architecture: request.RepositoryArchitectureX8664} + got := syncDownloadArgs(snapshot, "/tmp/dnf.conf", "/tmp/packages", []string{"x86_64", "noarch"}) + want := []string{"-q", "-c", "/tmp/dnf.conf", "--forcearch=x86_64", "reposync", + "--repoid=ocm-upstream", "--download-path=/tmp/packages", "--download-metadata", "--delete", + "--norepopath", "--arch=x86_64", "--arch=noarch"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("syncDownloadArgs() = %#v, want %#v", got, want) + } +} + +func TestSyncLimitsSelectExecutionModeAndAdministratorCeiling(t *testing.T) { + configuration := config.RepositoryConfig{MaxSyncBytes: 100, MaxSyncPackages: 20, + FilteredSyncBytes: 40, FilteredSyncPackages: 5} + fullBytes, fullPackages := syncLimits(configuration, request.RepositorySyncSnapshot{}) + if fullBytes != 100 || fullPackages != 20 { + t.Fatalf("full limits = %d/%d", fullBytes, fullPackages) + } + filteredBytes, filteredPackages := syncLimits(configuration, + request.RepositorySyncSnapshot{IncludePackages: []string{"curl"}}) + if filteredBytes != 40 || filteredPackages != 5 { + t.Fatalf("filtered limits = %d/%d", filteredBytes, filteredPackages) + } +} + +func TestUnmatchedPackageNamesParsesAndDeduplicatesDNFOutput(t *testing.T) { + output := []byte(strings.Join([]string{ + "stdout: No package missing-z available.", + "stderr: No package missing-a available.", + "No package missing-z available.", + "Last metadata expiration check: 0:00:01 ago.", + }, "\n")) + want := []string{"missing-a", "missing-z"} + if got := unmatchedPackageNames(output); !reflect.DeepEqual(got, want) { + t.Fatalf("unmatchedPackageNames() = %#v, want %#v", got, want) + } +} + +func TestSyncFilterSummaryIncludesUnmatchedPackagesAndCapsOutput(t *testing.T) { + unmatched := make([]string, 0, 22) + for index := 0; index < 22; index++ { + unmatched = append(unmatched, "missing-package-"+string(rune('a'+index))) + } + summary := syncFilterSummary(unmatched) + if !strings.Contains(summary, "missing-package-a") || !strings.Contains(summary, "(+2 more)") { + t.Fatalf("syncFilterSummary() = %q", summary) + } + if strings.Contains(summary, "missing-package-v") { + t.Fatalf("syncFilterSummary() did not cap the package list: %q", summary) + } +} + +func TestSyncDownloadArgsAppliesFiltersBeforeDownload(t *testing.T) { + snapshot := request.RepositorySyncSnapshot{ + Architecture: request.RepositoryArchitectureX8664, + IncludePackages: []string{"curl", "dnf-*"}, + ExcludePackages: []string{"*-devel"}, + } + got := syncDownloadArgs(snapshot, "/tmp/dnf.conf", "/tmp/packages", []string{"x86_64", "noarch"}) + want := []string{"-q", "-c", "/tmp/dnf.conf", "--forcearch=x86_64", "download", + "--repoid=ocm-upstream", "--destdir=/tmp/packages", "--arch=x86_64", "--arch=noarch", + "--exclude=*-devel", "--", "curl", "dnf-*"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("syncDownloadArgs() = %#v, want %#v", got, want) + } +} + +func TestSyncDNFConfigAppliesBandwidthAndParallelLimits(t *testing.T) { + snapshot := request.RepositorySyncSnapshot{UpstreamURL: "https://repo.example.invalid/oc9/", + BandwidthLimitBytes: 2 * 1024 * 1024, MaxParallelDownloads: 4} + got := syncDNFConfig(snapshot, "/tmp/repos.d", "/tmp/cache") + for _, expected := range []string{"throttle=2097152", "max_parallel_downloads=4", + "baseurl=https://repo.example.invalid/oc9/"} { + if !strings.Contains(got, expected) { + t.Fatalf("syncDNFConfig() does not contain %q:\n%s", expected, got) + } + } +} + +func TestNormalizeSyncSnapshotKeepsBackwardCompatibility(t *testing.T) { + value := request.RepositorySyncSnapshot{} + normalizeSyncSnapshot(&value) + if value.SyncMode != request.RepositorySyncModeMirror || value.MaxParallelDownloads != 3 { + t.Fatalf("normalizeSyncSnapshot() = %#v", value) + } +} + +func TestStoredPackageOriginalName(t *testing.T) { + if got := storedPackageOriginalName("0123456789abcdef-bash-1.0-1.x86_64.rpm"); got != "bash-1.0-1.x86_64.rpm" { + t.Fatalf("storedPackageOriginalName() = %q", got) + } +} diff --git a/manager/backend/services/footstone/repositoryworker/sync_integration_test.go b/manager/backend/services/footstone/repositoryworker/sync_integration_test.go new file mode 100644 index 0000000000000000000000000000000000000000..dce07324b1c7f895a3296ede413422e55b86919c --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/sync_integration_test.go @@ -0,0 +1,236 @@ +package repositoryworker + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/google/uuid" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/entity" + baseconfig "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/pkg/library/config/database" + ftpb "gitee.com/OpenCloudOS/ocmanager/manager/backend/protocol/footstone" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositorycontentrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/repositoryrepo" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryContentService" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryTaskService" + appconfig "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/config" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/infrastructure/repository/storage" +) + +func TestRemoteSyncPublishesAtomicSnapshot(t *testing.T) { + dsn := os.Getenv("OCM_REPOSITORY_INTEGRATION_DSN") + fixtures := []string{ + os.Getenv("OCM_REPOSITORY_TEST_RPM_ONE"), + os.Getenv("OCM_REPOSITORY_TEST_RPM_TWO"), + os.Getenv("OCM_REPOSITORY_TEST_RPM_X86"), + } + if dsn == "" || fixtures[0] == "" || fixtures[1] == "" || fixtures[2] == "" { + t.Skip("repository sync integration environment is not configured") + } + db, err := database.NewDBClient(baseconfig.AccessPoint{Source: dsn}) + if err != nil { + t.Fatal(err) + } + defer db.Close() + if err = db.DB().AutoMigrate(&po.Project{}); err != nil { + t.Fatal(err) + } + const projectID int64 = 900004 + if err = db.DB().Exec(`INSERT IGNORE INTO projects + (id, created_at, updated_at, name, en_name, creator, organization) + VALUES (?, NOW(), NOW(), 'repository-phase4', 'repository-phase4', 'admin', 'test')`, projectID).Error; err != nil { + t.Fatal(err) + } + + upstreamRoot := t.TempDir() + for _, source := range fixtures { + data, readErr := os.ReadFile(source) + if readErr != nil { + t.Fatal(readErr) + } + if writeErr := os.WriteFile(filepath.Join(upstreamRoot, filepath.Base(source)), data, 0644); writeErr != nil { + t.Fatal(writeErr) + } + } + if output, commandErr := exec.Command("createrepo_c", upstreamRoot).CombinedOutput(); commandErr != nil { + t.Fatalf("create upstream metadata: %v: %s", commandErr, output) + } + upstream := httptest.NewServer(http.FileServer(http.Dir(upstreamRoot))) + defer upstream.Close() + + repositories := repositoryrepo.New(db) + tasks := repositoryTaskService.New(db) + contentRepo := repositorycontentrepo.New(db) + if err = repositories.SyncTables(); err != nil { + t.Fatal(err) + } + if err = tasks.SyncTables(); err != nil { + t.Fatal(err) + } + if err = contentRepo.SyncTables(); err != nil { + t.Fatal(err) + } + repository := &po.SoftwareRepository{ + ID: uuid.NewString(), ProjectID: projectID, SystemProduct: "OpenCloudOS", ProductVersion: "9.6", + Name: "phase4-sync-" + uuid.NewString()[:8], Architecture: request.RepositoryArchitectureX8664, + Type: request.RepositoryTypeBaseline, UpstreamURL: upstream.URL, + StorageLocation: "public/phase4", PublishKey: uuid.NewString()[:32], + GPGPolicy: request.RepositoryGPGDisabled, Enabled: true, + PrimaryStorageNodeID: uuid.NewString(), + Status: request.RepositoryStatusEmpty, Creator: "admin", + } + if err = repository.SetPackageFilters([]string{"ocm-phase3-*"}, []string{"ocm-phase3-two"}); err != nil { + t.Fatal(err) + } + if err = repositories.Create(repository, nil); err != nil { + t.Fatal(err) + } + storeRoot := t.TempDir() + t.Cleanup(func() { makeIntegrationTreeWritable(storeRoot) }) + published := httptest.NewServer(http.FileServer(http.Dir(filepath.Join(storeRoot, "public")))) + defer published.Close() + store, err := storage.New(appconfig.RepositoryConfig{ + Root: storeRoot, MaxSyncBytes: 64 << 20, MaxSyncPackages: 10, + MetadataReserve: 1 << 20, MinimumFreeBytes: 1, PublicBaseURL: published.URL, + }) + if err != nil { + t.Fatal(err) + } + manager := &integrationRepositoryManager{repositories: map[string]*po.SoftwareRepository{repository.ID: repository}} + content := repositoryContentService.New(db, manager, tasks, store) + handler := NewHandler(db, store) + ctx := entity.NewDefaultContext() + ctx.User = &ftpb.UserInfo{LoginName: "admin", Roles: []*ftpb.ModuleRole{{RoleId: request.DefaultGlobalRoleName}}} + + firstTask, err := content.SyncRepository(ctx, repository.ID) + if err != nil { + t.Fatal(err) + } + var snapshot request.RepositorySyncSnapshot + if err = json.Unmarshal([]byte(firstTask.PayloadJSON), &snapshot); err != nil || + snapshot.UpstreamURL != upstream.URL || len(snapshot.ExcludePackages) != 1 { + t.Fatalf("sync snapshot=%+v err=%v", snapshot, err) + } + firstOutcome := executeIntegrationTask(t, tasks, handler, firstTask.ID) + if firstOutcome.Status != request.RepositoryTaskStatusSuccess { + t.Fatalf("first sync outcome=%+v", firstOutcome) + } + firstRevision, err := contentRepo.GetRevision(repository.ID, firstOutcome.RevisionID, nil) + if err != nil || firstRevision.Status != request.RepositoryRevisionStatusPublished || + firstRevision.PackageCount != 2 || firstRevision.RepomdSHA256 == "" || + firstRevision.SnapshotManifestSHA256 == "" { + t.Fatalf("first sync revision=%+v err=%v", firstRevision, err) + } + firstPackages, err := contentRepo.PackagesByRevision(firstRevision.ID, nil) + if err != nil || len(firstPackages) != 2 { + t.Fatalf("first sync packages=%+v err=%v", firstPackages, err) + } + for _, item := range firstPackages { + if item.Source != request.RepositoryPackageSourceSync || item.Name == "ocm-phase3-two" || + !architectureAllowed(repository.Architecture, item.Architecture) { + t.Fatalf("unexpected synced package=%+v", item) + } + } + t.Logf("sync revision=%s packages=%s repomd=%s manifest=%s", firstRevision.ID, + packageNEVRAs(firstPackages), firstRevision.RepomdSHA256, firstRevision.SnapshotManifestSHA256) + firstRepomd := filepath.Join(store.Root(), "public", repository.PublishKey, "revisions", + firstRevision.ID, "repodata", "repomd.xml") + firstRepomdSHA, _, err := fileSHA256(firstRepomd) + if err != nil || firstRepomdSHA != firstRevision.RepomdSHA256 { + t.Fatalf("first repomd sha=%s revision=%s err=%v", firstRepomdSHA, firstRevision.RepomdSHA256, err) + } + + secondTask, err := content.SyncRepository(ctx, repository.ID) + if err != nil { + t.Fatal(err) + } + secondOutcome := executeIntegrationTask(t, tasks, handler, secondTask.ID) + if secondOutcome.Status != request.RepositoryTaskStatusSuccess || secondOutcome.RevisionID == firstRevision.ID { + t.Fatalf("second sync outcome=%+v", secondOutcome) + } + current := filepath.Join(store.Root(), "public", repository.PublishKey, "current") + if target, readErr := os.Readlink(current); readErr != nil || + filepath.ToSlash(target) != "revisions/"+secondOutcome.RevisionID { + t.Fatalf("current target=%q err=%v", target, readErr) + } + if unchangedSHA, _, hashErr := fileSHA256(firstRepomd); hashErr != nil || unchangedSHA != firstRepomdSHA { + t.Fatalf("immutable first revision sha=%s want=%s err=%v", unchangedSHA, firstRepomdSHA, hashErr) + } + t.Logf("atomic switch first=%s second=%s current=revisions/%s old_repomd_unchanged=true", + firstRevision.ID, secondOutcome.RevisionID, secondOutcome.RevisionID) + + publishedRepository, err := repositories.GetByID(repository.ID, nil) + if err != nil { + t.Fatal(err) + } + manager.repositories[repository.ID] = publishedRepository + generated, err := content.GenerateRepositoryConfig(ctx, repository.ID, true) + if err != nil { + t.Fatal(err) + } + if generated.ActiveRevisionID != secondOutcome.RevisionID || + generated.BaseURL != published.URL+"/"+repository.PublishKey+"/revisions/"+secondOutcome.RevisionID+"/" || + !generated.ImmutableRevision { + t.Fatalf("generated repository config=%+v", generated) + } + assertDNFRepositoryConfig(t, generated.RepoID, generated.Content) + t.Logf("generated repo id=%s baseurl=%s revision_url=%s", generated.RepoID, + generated.BaseURL, generated.RevisionURL) + + manager.repositories[repository.ID].UpstreamURL = "http://127.0.0.1:1/unreachable" + failedTask, err := content.SyncRepository(ctx, repository.ID) + if err != nil { + t.Fatal(err) + } + failedOutcome := executeIntegrationTask(t, tasks, handler, failedTask.ID) + if failedOutcome.Status != request.RepositoryTaskStatusFailed || failedOutcome.ErrorCode != "SYNC_DOWNLOAD_FAILED" { + t.Fatalf("failed sync outcome=%+v", failedOutcome) + } + storedRepository, err := repositories.GetByID(repository.ID, nil) + if err != nil || storedRepository.ActiveRevisionID != secondOutcome.RevisionID { + t.Fatalf("failed sync changed active repository=%+v err=%v", storedRepository, err) + } + t.Logf("failed sync status=%s error=%s active_unchanged=%s", + failedOutcome.Status, failedOutcome.ErrorCode, storedRepository.ActiveRevisionID) +} + +func packageNEVRAs(values []po.RepositoryPackage) string { + nevras := make([]string, 0, len(values)) + for _, value := range values { + nevras = append(nevras, value.NEVRA) + } + return strings.Join(nevras, ",") +} + +func assertDNFRepositoryConfig(t *testing.T, repoID, repositoryConfig string) { + t.Helper() + root := t.TempDir() + configPath := filepath.Join(root, "dnf.conf") + configValue := fmt.Sprintf(`[main] +reposdir=%s +cachedir=%s +keepcache=0 + +%s`, filepath.Join(root, "repos.d"), filepath.Join(root, "cache"), repositoryConfig) + if err := os.MkdirAll(filepath.Join(root, "repos.d"), 0700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(configPath, []byte(configValue), 0600); err != nil { + t.Fatal(err) + } + command := exec.Command("dnf", "-q", "-c", configPath, "--disablerepo=*", "--enablerepo="+repoID, + "makecache", "--refresh") + if output, err := command.CombinedOutput(); err != nil { + t.Fatalf("isolated dnf makecache: %v: %s", err, strings.TrimSpace(string(output))) + } +} diff --git a/manager/backend/services/footstone/repositoryworker/testdata/e2e/ocm-private-app.spec b/manager/backend/services/footstone/repositoryworker/testdata/e2e/ocm-private-app.spec new file mode 100644 index 0000000000000000000000000000000000000000..e14879d99a59169c29c9ac85003689f278658886 --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/testdata/e2e/ocm-private-app.spec @@ -0,0 +1,21 @@ +Name: ocm-private-app +Version: 1.0.0 +Release: 1.oc9 +Summary: OCManager Phase 7 private application fixture +License: MIT +BuildArch: noarch +Requires: ocm-sync-dependency = 1.0.0-1.oc9 + +%description +Private application whose dependency is available only from the Phase 7 sync repository. + +%prep + +%build + +%install +mkdir -p %{buildroot}/usr/share/ocmanager-e2e +printf 'private-app\n' > %{buildroot}/usr/share/ocmanager-e2e/ocm-private-app.txt + +%files +/usr/share/ocmanager-e2e/ocm-private-app.txt diff --git a/manager/backend/services/footstone/repositoryworker/testdata/e2e/ocm-private-only.spec b/manager/backend/services/footstone/repositoryworker/testdata/e2e/ocm-private-only.spec new file mode 100644 index 0000000000000000000000000000000000000000..e6dddd077244ba0b09741d7b44633117a15a47b5 --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/testdata/e2e/ocm-private-only.spec @@ -0,0 +1,20 @@ +Name: ocm-private-only +Version: 1.0.0 +Release: 1.oc9 +Summary: OCManager Phase 7 private-only fixture +License: MIT +BuildArch: noarch + +%description +Package available only from the OCManager Phase 7 private repository. + +%prep + +%build + +%install +mkdir -p %{buildroot}/usr/share/ocmanager-e2e +printf 'private-only\n' > %{buildroot}/usr/share/ocmanager-e2e/ocm-private-only.txt + +%files +/usr/share/ocmanager-e2e/ocm-private-only.txt diff --git a/manager/backend/services/footstone/repositoryworker/testdata/e2e/ocm-sync-dependency.spec b/manager/backend/services/footstone/repositoryworker/testdata/e2e/ocm-sync-dependency.spec new file mode 100644 index 0000000000000000000000000000000000000000..a7fe74413d513fc4206ad5bc83b93516ecc43006 --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/testdata/e2e/ocm-sync-dependency.spec @@ -0,0 +1,20 @@ +Name: ocm-sync-dependency +Version: 1.0.0 +Release: 1.oc9 +Summary: OCManager Phase 7 synchronized dependency fixture +License: MIT +BuildArch: noarch + +%description +Dependency available only from the OCManager Phase 7 sync repository. + +%prep + +%build + +%install +mkdir -p %{buildroot}/usr/share/ocmanager-e2e +printf 'sync-dependency\n' > %{buildroot}/usr/share/ocmanager-e2e/ocm-sync-dependency.txt + +%files +/usr/share/ocmanager-e2e/ocm-sync-dependency.txt diff --git a/manager/backend/services/footstone/repositoryworker/testdata/e2e/ocm-sync-only.spec b/manager/backend/services/footstone/repositoryworker/testdata/e2e/ocm-sync-only.spec new file mode 100644 index 0000000000000000000000000000000000000000..2847d527f5c6d5f0ad840991a22faf6859ef2c1d --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/testdata/e2e/ocm-sync-only.spec @@ -0,0 +1,20 @@ +Name: ocm-sync-only +Version: 1.0.0 +Release: 1.oc9 +Summary: OCManager Phase 7 sync-only fixture +License: MIT +BuildArch: noarch + +%description +Package available only from the OCManager Phase 7 sync repository. + +%prep + +%build + +%install +mkdir -p %{buildroot}/usr/share/ocmanager-e2e +printf 'sync-only\n' > %{buildroot}/usr/share/ocmanager-e2e/ocm-sync-only.txt + +%files +/usr/share/ocmanager-e2e/ocm-sync-only.txt diff --git a/manager/backend/services/footstone/repositoryworker/worker.go b/manager/backend/services/footstone/repositoryworker/worker.go new file mode 100644 index 0000000000000000000000000000000000000000..b94822b35174adca16f38558f71ded1ac891166d --- /dev/null +++ b/manager/backend/services/footstone/repositoryworker/worker.go @@ -0,0 +1,162 @@ +package repositoryworker + +import ( + "context" + "errors" + "fmt" + "time" + + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/entity/request" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repository/po" + "gitee.com/OpenCloudOS/ocmanager/manager/backend/services/footstone/domain/repositoryTaskService" + "trpc.group/trpc-go/trpc-go/log" +) + +type Worker struct { + id string + tasks repositoryTaskService.Service + handler *Handler + lease time.Duration + pollInterval time.Duration +} + +func NewWorker(id string, tasks repositoryTaskService.Service, handler *Handler, + lease, pollInterval time.Duration, +) (*Worker, error) { + if id == "" { + return nil, fmt.Errorf("worker id is required") + } + if lease < 15*time.Second { + return nil, fmt.Errorf("worker lease must be at least 15 seconds") + } + if pollInterval <= 0 { + pollInterval = 2 * time.Second + } + return &Worker{id: id, tasks: tasks, handler: handler, lease: lease, pollInterval: pollInterval}, nil +} + +func (w *Worker) Run(ctx context.Context) error { + recoveryTicker := time.NewTicker(10 * time.Second) + defer recoveryTicker.Stop() + for { + select { + case <-ctx.Done(): + return nil + case <-recoveryTicker.C: + if recovered, err := w.tasks.RecoverExpired(); err != nil { + log.Errorf("repository worker recover expired tasks: %v", err) + } else if recovered > 0 { + log.Infof("repository worker recovered %d expired tasks", recovered) + } + default: + } + task, err := w.tasks.ClaimNext(w.id, w.lease) + if err != nil { + return fmt.Errorf("claim repository task: %w", err) + } + if task == nil { + select { + case <-ctx.Done(): + return nil + case <-time.After(w.pollInterval): + } + continue + } + if err = w.execute(ctx, task); err != nil { + log.Errorf("repository worker task %s: %v", task.ID, err) + } + } +} + +func (w *Worker) execute(parent context.Context, task *po.RepositoryTask) error { + ctx, cancel := context.WithCancel(parent) + defer cancel() + stopHeartbeat := make(chan struct{}) + heartbeatResult := make(chan error, 1) + cancelRequested := make(chan struct{}, 1) + go w.heartbeat(ctx, task, stopHeartbeat, heartbeatResult, cancelRequested, cancel) + + outcome := w.handler.Handle(ctx, task) + close(stopHeartbeat) + heartbeatErr := <-heartbeatResult + if heartbeatErr != nil { + return heartbeatErr + } + select { + case <-cancelRequested: + outcome = Outcome{Status: request.RepositoryTaskStatusCanceled, + ErrorCode: "CANCELED_BY_USER", ErrorMessage: "repository task cancellation was requested"} + default: + } + if outcome.RevisionID != "" { + if _, err := w.tasks.AttachRevision(request.AttachRepositoryTaskRevisionRequest{ + TaskID: task.ID, WorkerID: w.id, FencingToken: task.FencingToken, + RevisionID: outcome.RevisionID, + }); err != nil { + return err + } + } + _, err := w.tasks.Complete(request.CompleteRepositoryTaskRequest{ + TaskID: task.ID, WorkerID: w.id, FencingToken: task.FencingToken, + Status: outcome.Status, ErrorCode: truncateText(outcome.ErrorCode, 64), + ErrorMessage: truncateText(outcome.ErrorMessage, 2048), + }) + if err != nil { + return err + } + if finalizeErr := w.handler.FinalizeUploadOutcome(task, outcome); finalizeErr != nil { + log.Errorf("repository worker task %s: finalize upload outcome: %v", task.ID, finalizeErr) + } + return nil +} + +func (w *Worker) heartbeat(ctx context.Context, task *po.RepositoryTask, stop <-chan struct{}, + result chan<- error, cancelRequested chan<- struct{}, cancel context.CancelFunc, +) { + ticker := time.NewTicker(w.lease / 3) + defer ticker.Stop() + for { + select { + case <-stop: + result <- nil + return + case <-ctx.Done(): + select { + case <-stop: + result <- nil + default: + result <- ctx.Err() + } + return + case <-ticker.C: + value, err := w.tasks.Heartbeat(request.HeartbeatRepositoryTaskRequest{ + TaskID: task.ID, WorkerID: w.id, FencingToken: task.FencingToken, + LeaseSeconds: int(w.lease / time.Second), ProgressPercent: task.ProgressPercent, + CurrentStep: "processing repository content", + }) + if err != nil { + if !errors.Is(err, repositoryTaskService.ErrTaskOwnershipLost) { + cancel() + } + result <- err + return + } + if value.Status == request.RepositoryTaskStatusCancelRequested { + select { + case cancelRequested <- struct{}{}: + default: + } + cancel() + result <- nil + return + } + } + } +} + +func truncateText(value string, maximum int) string { + if len(value) <= maximum { + return value + } + return value[:maximum] +} diff --git a/manager/backend/services/msg-etl/deployments/docker/Dockerfile b/manager/backend/services/msg-etl/deployments/docker/Dockerfile index 7001b6e1748fce2221e4165cf8172eb94a44e243..33096becaae247798794ef6757f7eda51ccd28f6 100755 --- a/manager/backend/services/msg-etl/deployments/docker/Dockerfile +++ b/manager/backend/services/msg-etl/deployments/docker/Dockerfile @@ -1,4 +1,5 @@ -FROM mirror.ccs.tencentyun.com/opencloudos/opencloudos9-minimal:latest +ARG RUNTIME_IMAGE=docker.io/opencloudos/opencloudos9-minimal:9.6-v20260714 +FROM ${RUNTIME_IMAGE} # 复制项目文件 ENV DEST=/home/ocmanager diff --git a/manager/backend/services/sysdiagnose/dependence/tms/gateway_signature.go b/manager/backend/services/sysdiagnose/dependence/tms/gateway_signature.go new file mode 100644 index 0000000000000000000000000000000000000000..bb042d520751f3a8be4a3eff6de902231271680d --- /dev/null +++ b/manager/backend/services/sysdiagnose/dependence/tms/gateway_signature.go @@ -0,0 +1,103 @@ +// 网关 HMAC 签名(issue #IK4AMG)。 +// +// sysdiagnose 直连 tms/api 的 /api/v1/ops_tasks/* 与 /api/v1/artifacts/upload。 +// tms/api 在配置了共享密钥后要求状态变更端点携带签名(STRICT=true 时全部端点都要), +// 因此本包必须与 tms/api internal/middleware 的规范化串保持一致,否则调用会被判为 +// invalid gateway signature。 +package tms + +import ( + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "fmt" + "net/http" + "os" + "strconv" + "strings" + "sync" + "time" + + "trpc.group/trpc-go/trpc-go/log" +) + +const ( + // EnvGatewayHMACSecret 与 tms/api、footstone tmsclient 共用的环境变量名。 + EnvGatewayHMACSecret = "TMS_API_GW_HMAC_SECRET" + + // HeaderUserRole 参与规范化串的调用方角色头。 + HeaderUserRole = "X-User-Role" + // HeaderSignatureTimestamp / HeaderSignatureNonce / HeaderSignature 为签名三元头。 + HeaderSignatureTimestamp = "X-Signature-Timestamp" + HeaderSignatureNonce = "X-Signature-Nonce" + HeaderSignature = "X-Signature" + + // defaultGatewayUserRole 与 tmsclient 及 tms/api 的缺省角色保持一致。 + defaultGatewayUserRole = "user" + + // maxSignedBodyBytes 必须不大于 tms/api 中间件读取请求体计算摘要的上限, + // 超过后服务端无法还原出同一份字节,签名必然校验失败。 + maxSignedBodyBytes = 8 << 20 +) + +var unsignedWarnOnce sync.Once + +// gatewayHMACSecret 返回共享密钥;空串表示网关签名未启用。 +// 每次调用都重新读取,便于测试临时覆盖,也避免进程启动顺序造成密钥“读早了”。 +func gatewayHMACSecret() string { + return strings.TrimSpace(os.Getenv(EnvGatewayHMACSecret)) +} + +// signGatewayRequest 用共享密钥对请求做规范化签名并写入三个签名头。 +// +// body 必须是即将发送的完整请求体字节;无体请求传 nil。密钥为空时不签名并返回 nil, +// 同时打印一次 WARN——该退化只适用于开发/测试,正式部署必须配置非空密钥。 +func signGatewayRequest(req *http.Request, userName string, body []byte) error { + if req == nil { + return nil + } + secret := gatewayHMACSecret() + if secret == "" { + unsignedWarnOnce.Do(func() { + log.Warnf("tms gateway HMAC secret is empty: requests are sent UNSIGNED; " + + "this is only acceptable for development. Set " + EnvGatewayHMACSecret + + " and keep TMS_API_GW_HMAC_STRICT=true for production.") + }) + return nil + } + if int64(len(body)) > maxSignedBodyBytes { + return fmt.Errorf("tms request body is %d bytes, exceeding the gateway signing limit %d: "+ + "the api cannot hash a larger body, so the request would be rejected", + len(body), maxSignedBodyBytes) + } + + role := req.Header.Get(HeaderUserRole) + if role == "" { + role = defaultGatewayUserRole + } + nonceBytes := make([]byte, 16) + if _, err := rand.Read(nonceBytes); err != nil { + return fmt.Errorf("generate tms request nonce: %w", err) + } + timestamp := strconv.FormatInt(time.Now().Unix(), 10) + nonce := hex.EncodeToString(nonceBytes) + sum := sha256.Sum256(body) + + mac := hmac.New(sha256.New, []byte(secret)) + // 规范化串必须与 tms/api verifyGatewaySignature 完全一致: + // method\npath\nuser\nrole\ntimestamp\nnonce\nsha256hex(body) + fmt.Fprintf(mac, "%s\n%s\n%s\n%s\n%s\n%s\n%s", req.Method, req.URL.Path, + userName, role, timestamp, nonce, hex.EncodeToString(sum[:])) + + req.Header.Set(HeaderUserRole, role) + req.Header.Set(HeaderSignatureTimestamp, timestamp) + req.Header.Set(HeaderSignatureNonce, nonce) + req.Header.Set(HeaderSignature, hex.EncodeToString(mac.Sum(nil))) + return nil +} + +// gatewaySignatureEnabled 报告当前是否需要为请求签名。 +func gatewaySignatureEnabled() bool { + return gatewayHMACSecret() != "" +} diff --git a/manager/backend/services/sysdiagnose/dependence/tms/gateway_signature_test.go b/manager/backend/services/sysdiagnose/dependence/tms/gateway_signature_test.go new file mode 100644 index 0000000000000000000000000000000000000000..c7847036bd78992ddc61fd0d87e51c7b2a027d10 --- /dev/null +++ b/manager/backend/services/sysdiagnose/dependence/tms/gateway_signature_test.go @@ -0,0 +1,277 @@ +package tms + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "io" + "mime" + "mime/multipart" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strconv" + "strings" + "testing" + "time" +) + +// verifyGatewaySignature mirrors tms/api/internal/middleware.verifyGatewaySignature. +// Kept here on purpose: if the client canonicalization drifts from the server, the +// golden test below fails instead of only breaking in a live deployment. +func verifyGatewaySignature(t *testing.T, secret, method, p, userName, userRole, ts, nonce, bodyDigest, sigHex string) { + t.Helper() + sig, err := hex.DecodeString(sigHex) + if err != nil || len(sig) == 0 { + t.Fatalf("signature hex invalid: %v", err) + } + mac := hmac.New(sha256.New, []byte(secret)) + _, werr := mac.Write([]byte(method + "\n" + p + "\n" + userName + "\n" + userRole + "\n" + + ts + "\n" + nonce + "\n" + bodyDigest)) + if werr != nil { + t.Fatalf("write mac: %v", werr) + } + if !hmac.Equal(sig, mac.Sum(nil)) { + t.Fatalf("gateway signature mismatch") + } +} + +func TestSignGatewayRequestMatchesServerCanonicalization(t *testing.T) { + t.Setenv(EnvGatewayHMACSecret, "s3cr3t-shared-with-api") + payload := []byte(`{"action":"run_artifact"}`) + + req, err := http.NewRequest(http.MethodPost, "http://tms.test/api/v1/ops_tasks/publish", bytes.NewReader(payload)) + if err != nil { + t.Fatal(err) + } + req.Header.Set(HeaderXUserName, "X-OCManager") + if err := signGatewayRequest(req, "X-OCManager", payload); err != nil { + t.Fatalf("signGatewayRequest() error: %v", err) + } + + ts, nonce, sig := req.Header.Get(HeaderSignatureTimestamp), req.Header.Get(HeaderSignatureNonce), req.Header.Get(HeaderSignature) + if ts == "" || nonce == "" || sig == "" { + t.Fatalf("signature headers missing: ts=%q nonce=%q sig=%q", ts, nonce, sig) + } + if got := req.Header.Get(HeaderUserRole); got != defaultGatewayUserRole { + t.Errorf("%s = %q, want %q", HeaderUserRole, got, defaultGatewayUserRole) + } + if _, err := strconv.ParseInt(ts, 10, 64); err != nil { + t.Errorf("timestamp not an integer: %v", err) + } + if skew := time.Now().Unix() - mustInt(ts); skew < 0 || skew > 300 { + t.Errorf("timestamp skew = %ds, want within 300s", skew) + } + sum := sha256.Sum256(payload) + verifyGatewaySignature(t, "s3cr3t-shared-with-api", http.MethodPost, "/api/v1/ops_tasks/publish", + "X-OCManager", defaultGatewayUserRole, ts, nonce, hex.EncodeToString(sum[:]), sig) +} + +func TestSignGatewayRequestEmptyBodyDigestIsSha256OfEmpty(t *testing.T) { + t.Setenv(EnvGatewayHMACSecret, "k") + req, err := http.NewRequest(http.MethodGet, "http://tms.test/api/v1/ops_tasks/query", nil) + if err != nil { + t.Fatal(err) + } + if err := signGatewayRequest(req, "u", nil); err != nil { + t.Fatalf("signGatewayRequest() error: %v", err) + } + empty := sha256.Sum256(nil) + verifyGatewaySignature(t, "k", http.MethodGet, "/api/v1/ops_tasks/query", "u", defaultGatewayUserRole, + req.Header.Get(HeaderSignatureTimestamp), req.Header.Get(HeaderSignatureNonce), + hex.EncodeToString(empty[:]), req.Header.Get(HeaderSignature)) +} + +func TestSignGatewayRequestSkippedWithoutSecret(t *testing.T) { + t.Setenv(EnvGatewayHMACSecret, " ") + req, err := http.NewRequest(http.MethodPost, "http://tms.test/api/v1/ops_tasks/publish", strings.NewReader("{}")) + if err != nil { + t.Fatal(err) + } + if err := signGatewayRequest(req, "u", []byte("{}")); err != nil { + t.Fatalf("signGatewayRequest() error: %v", err) + } + for _, h := range []string{HeaderSignature, HeaderSignatureNonce, HeaderSignatureTimestamp} { + if got := req.Header.Get(h); got != "" { + t.Errorf("%s = %q, want empty when the secret is unset", h, got) + } + } +} + +func TestSignGatewayRequestRejectsOversizedBody(t *testing.T) { + t.Setenv(EnvGatewayHMACSecret, "k") + req, err := http.NewRequest(http.MethodPost, "http://tms.test/api/v1/artifacts/upload", nil) + if err != nil { + t.Fatal(err) + } + big := make([]byte, maxSignedBodyBytes+1) + if err := signGatewayRequest(req, "u", big); err == nil { + t.Fatal("expected an error for a body larger than the api digest limit, got nil") + } + if got := req.Header.Get(HeaderSignature); got != "" { + t.Errorf("no signature should be attached when validation fails, got %q", got) + } +} + +func TestSignGatewayRequestUsesFreshNoncePerAttempt(t *testing.T) { + t.Setenv(EnvGatewayHMACSecret, "k") + seen := map[string]bool{} + for i := 0; i < 5; i++ { + req, err := http.NewRequest(http.MethodPost, "http://tms.test/api/v1/ops_tasks/publish", strings.NewReader("{}")) + if err != nil { + t.Fatal(err) + } + if err := signGatewayRequest(req, "u", []byte("{}")); err != nil { + t.Fatal(err) + } + nonce := req.Header.Get(HeaderSignatureNonce) + if seen[nonce] { + t.Fatalf("nonce %s reused across attempts; tms/api would reject the retry as a replay", nonce) + } + seen[nonce] = true + } +} + +func TestSetParamStreamsWhenSignatureDisabled(t *testing.T) { + t.Setenv(EnvGatewayHMACSecret, "") + filePath := writeArtifact(t, 4096) + + req, err := newTestClient("http://tms.test").setParam(&UploadRequest{FilePath: filePath}) + if err != nil { + t.Fatalf("setParam() error: %v", err) + } + defer req.Body.Close() + if _, ok := req.Body.(*multipartBody); !ok { + t.Fatalf("request body type = %T, want the streaming *multipartBody when unsigned", req.Body) + } + if got := req.Header.Get(HeaderSignature); got != "" { + t.Errorf("unsigned request carried %q", got) + } +} + +func TestSetParamSignsAndStaysReplayableWhenEnabled(t *testing.T) { + t.Setenv(EnvGatewayHMACSecret, "k") + filePath := writeArtifact(t, 4096) + body, err := os.ReadFile(filePath) + if err != nil { + t.Fatal(err) + } + + client := newTestClient("http://tms.test") + req, err := client.setParam(&UploadRequest{FilePath: filePath}) + if err != nil { + t.Fatalf("setParam() error: %v", err) + } + if req.GetBody == nil { + t.Fatal("GetBody is nil; retries could not re-sign the same bytes") + } + sent, err := io.ReadAll(req.Body) + if err != nil { + t.Fatal(err) + } + req.Body.Close() + + // The body must still parse as the multipart payload the api will hash. + _, params, err := mime.ParseMediaType(req.Header.Get("Content-Type")) + if err != nil { + t.Fatalf("Content-Type: %v", err) + } + part, err := multipart.NewReader(bytes.NewReader(sent), params["boundary"]).NextPart() + if err != nil { + t.Fatalf("multipart parse: %v", err) + } + got, err := io.ReadAll(part) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, body) { + t.Fatalf("uploaded file part differs from the source artifact") + } + + replay, err := req.GetBody() + if err != nil { + t.Fatal(err) + } + again, err := io.ReadAll(replay) + if err != nil { + t.Fatal(err) + } + replay.Close() + if !bytes.Equal(again, sent) { + t.Fatal("GetBody did not replay identical bytes, so a re-signed retry would not verify") + } + + sum := sha256.Sum256(sent) + verifyGatewaySignature(t, "k", http.MethodPost, "/api/v1/artifacts/upload", client.UserID, defaultGatewayUserRole, + req.Header.Get(HeaderSignatureTimestamp), req.Header.Get(HeaderSignatureNonce), + hex.EncodeToString(sum[:]), req.Header.Get(HeaderSignature)) +} + +func TestSetParamRejectsArtifactAboveSigningLimit(t *testing.T) { + t.Setenv(EnvGatewayHMACSecret, "k") + dir := t.TempDir() + filePath := filepath.Join(dir, "huge.bin") + f, err := os.Create(filePath) + if err != nil { + t.Fatal(err) + } + if err := f.Truncate(maxSignedBodyBytes + 1); err != nil { + t.Fatal(err) + } + f.Close() + + if _, err := newTestClient("http://tms.test").setParam(&UploadRequest{FilePath: filePath}); err == nil { + t.Fatal("expected an error before buffering an oversized artifact") + } +} + +// TestUploadSendsSignatureToServer exercises the whole client against a stub api. +func TestUploadSendsSignatureToServer(t *testing.T) { + t.Setenv(EnvGatewayHMACSecret, "k") + filePath := writeArtifact(t, 2048) + + var got *http.Request + var gotBody []byte + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + got = r + gotBody, _ = io.ReadAll(r.Body) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"code":"OK","message":"ok","data":{"md5":"abc"}}`)) + })) + defer srv.Close() + + c := newTestClient(srv.URL) + md5, err := c.Upload(&UploadRequest{FilePath: filePath}) + if err != nil { + t.Fatalf("Upload() error: %v", err) + } + if md5 != "abc" { + t.Errorf("md5 = %q, want abc", md5) + } + if got == nil || got.Header.Get(HeaderSignature) == "" { + t.Fatal("server saw no gateway signature header") + } + if got.Header.Get(HeaderXUserName) == "" { + t.Error("server saw no identity header") + } + sum := sha256.Sum256(gotBody) + verifyGatewaySignature(t, "k", http.MethodPost, "/api/v1/artifacts/upload", c.UserID, defaultGatewayUserRole, + got.Header.Get(HeaderSignatureTimestamp), got.Header.Get(HeaderSignatureNonce), + hex.EncodeToString(sum[:]), got.Header.Get(HeaderSignature)) +} + +func writeArtifact(t *testing.T, size int) string { + t.Helper() + filePath := filepath.Join(t.TempDir(), "artifact.bin") + if err := os.WriteFile(filePath, bytes.Repeat([]byte("x"), size), 0o600); err != nil { + t.Fatal(err) + } + return filePath +} + +func mustInt(s string) int64 { + v, _ := strconv.ParseInt(s, 10, 64) + return v +} diff --git a/manager/backend/services/sysdiagnose/dependence/tms/tms.go b/manager/backend/services/sysdiagnose/dependence/tms/tms.go index 8fa0486e0cd3600fd66107b9a9539aa219ebfb98..10fffea8e94020ae5903a425261f87ad3120b6b9 100644 --- a/manager/backend/services/sysdiagnose/dependence/tms/tms.go +++ b/manager/backend/services/sysdiagnose/dependence/tms/tms.go @@ -2,7 +2,8 @@ // // 对接接口:ocmanager `/api/v1/ops_tasks/*` 与 `/api/v1/artifacts/upload`。 // -// 鉴权:通过 HTTP Header `X-User-Name` 传递调用方身份。 +// 鉴权:通过 HTTP Header `X-User-Name` 传递调用方身份;配置了 TMS_API_GW_HMAC_SECRET 时 +// 额外携带网关 HMAC 签名(见 gateway_signature.go),以满足 tms/api 对状态变更端点的签名要求。 // // 服务连接:使用直连 URL,Config.Service 字段即 HTTP 根地址(如 http://host:port)。 // @@ -268,7 +269,8 @@ func (c *Client) doJSONRequest(ctx entity.Context, subPath string, body interfac return nil, pkgerr.WrapStackError(err) } - // build 每次重试都重建请求(bytes.NewReader 对同一 payload 可重复读取) + // build 每次重试都重建请求(bytes.NewReader 对同一 payload 可重复读取)。 + // 签名也必须逐次重算:tms/api 用 nonce 防重放,复用同一签名会被判为重放。 build := func() (*http.Request, error) { req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(payload)) if err != nil { @@ -279,6 +281,9 @@ func (c *Client) doJSONRequest(ctx entity.Context, subPath string, body interfac if ctx.RequestID != "" { req.Header.Set("X-Request-Id", ctx.RequestID) } + if err := signGatewayRequest(req, c.UserID, payload); err != nil { + return nil, pkgerr.WrapStackError(err) + } return req, nil } @@ -317,6 +322,13 @@ func (c *Client) setParam(param *UploadRequest) (*http.Request, error) { return nil, pkgerr.WrapStackError(err) } + // 网关签名需要对完整请求体求摘要,因此启用签名时物化 multipart 正文; + // 大小受 tms/api 摘要读取上限约束,超限直接报错而不是发出必然校验失败的请求。 + // 未启用签名时保持原有流式上传,不额外占用内存。 + if gatewaySignatureEnabled() { + return c.setParamBuffered(param, url) + } + file, err := os.Open(param.FilePath) if err != nil { return nil, pkgerr.WrapStackError(err) @@ -365,6 +377,51 @@ func (c *Client) setParam(param *UploadRequest) (*http.Request, error) { return req, nil } +// setParamBuffered 构造可重放的 multipart 上传请求,并对完整正文计算网关签名。 +// +// 与流式版本的区别:正文先落到内存,因此每次重试都能用同一份字节配新的 nonce/时间戳重签。 +// 正文大小受 maxSignedBodyBytes 约束(tms/api 只对不超过该上限的请求体求摘要)。 +func (c *Client) setParamBuffered(param *UploadRequest, url string) (*http.Request, error) { + info, err := os.Stat(param.FilePath) + if err != nil { + return nil, pkgerr.WrapStackError(err) + } + if info.Size() > maxSignedBodyBytes { + return nil, fmt.Errorf("tms upload artifact %s is %d bytes, exceeding the gateway signing limit %d", + path.Base(param.FilePath), info.Size(), maxSignedBodyBytes) + } + + file, err := os.Open(param.FilePath) + if err != nil { + return nil, pkgerr.WrapStackError(err) + } + defer file.Close() + + var buf bytes.Buffer + writer := multipart.NewWriter(&buf) + part, err := writer.CreateFormFile("file", path.Base(param.FilePath)) + if err != nil { + return nil, pkgerr.WrapStackError(err) + } + if _, err := io.Copy(part, file); err != nil { + return nil, pkgerr.WrapStackError(err) + } + if err := writer.Close(); err != nil { + return nil, pkgerr.WrapStackError(err) + } + + req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(buf.Bytes())) + if err != nil { + return nil, pkgerr.WrapStackError(err) + } + req.Header.Set("Content-Type", writer.FormDataContentType()) + attachIdentityHeader(req, c.UserID) + if err := signGatewayRequest(req, c.UserID, buf.Bytes()); err != nil { + return nil, pkgerr.WrapStackError(err) + } + return req, nil +} + // computeFileMD5 计算文件内容的 MD5(十六进制小写)。 // 用于 Upload 降级:当 TMS 不可达时,校验本地文件是否与已配置的制品 MD5 一致, // 若一致则可直接返回该 MD5(TMS 内容寻址存储保证相同 MD5 即相同制品)。 diff --git a/manager/backend/services/sysdiagnose/deployments/docker/Dockerfile b/manager/backend/services/sysdiagnose/deployments/docker/Dockerfile index c16eb5080a66d4fc187ea2cb953fd6b082768abe..32e41763f2b0f2836080bc9ad7bda5b6666857b4 100644 --- a/manager/backend/services/sysdiagnose/deployments/docker/Dockerfile +++ b/manager/backend/services/sysdiagnose/deployments/docker/Dockerfile @@ -1,4 +1,5 @@ -FROM mirror.ccs.tencentyun.com/opencloudos/opencloudos9-minimal:latest +ARG RUNTIME_IMAGE=docker.io/opencloudos/opencloudos9-minimal:9.6-v20260714 +FROM ${RUNTIME_IMAGE} # 复制项目文件 diff --git a/manager/deployments/compose/Dockerfile.frontend b/manager/deployments/compose/Dockerfile.frontend index 49b3f3dbbfc89a8437837ffb503c8e0136b5e9a5..cae307dc6e375f97a6c8d908ec0d9c15dd0ede12 100644 --- a/manager/deployments/compose/Dockerfile.frontend +++ b/manager/deployments/compose/Dockerfile.frontend @@ -1,4 +1,4 @@ -# ocmanager 前端多阶段构建(node/nginx 基镜像经 mirror.ccs.tencentyun.com) +# ocmanager 前端多阶段构建 # # 使用方式(在 manager/frontend 目录执行): # docker build -f ../deployments/compose/Dockerfile.frontend -t oc-frontend . @@ -6,16 +6,22 @@ # 生产部署推荐通过 docker-compose.yml 统一构建与管理,详见 # manager/deployments/compose/docker-compose.yml -# 基镜像可经 compose 的 NODE_IMAGE / NGINX_IMAGE 构建参数覆盖(默认腾讯云镜像源) -ARG NODE_IMAGE=mirror.ccs.tencentyun.com/library/node:18-alpine -ARG NGINX_IMAGE=mirror.ccs.tencentyun.com/library/nginx:1.25-alpine +# 基镜像可经 compose 的 NODE_IMAGE / NGINX_IMAGE 构建参数覆盖。 +ARG NODE_IMAGE=docker.io/library/node:20-alpine +ARG NGINX_IMAGE=docker.io/library/nginx:1.25-alpine # ── Stage 1: 构建前端 ──────────────────────────────────────────────────────── FROM ${NODE_IMAGE} AS builder +ARG NPM_REGISTRY=https://registry.npmjs.org + WORKDIR /build COPY package*.json ./ -RUN npm ci --legacy-peer-deps +RUN npm config set registry "${NPM_REGISTRY}" \ + && npm config set fetch-retries 5 \ + && npm config set fetch-retry-mintimeout 10000 \ + && npm config set fetch-retry-maxtimeout 120000 \ + && npm ci --legacy-peer-deps --no-audit --no-fund COPY . . RUN npm run build @@ -23,7 +29,13 @@ RUN npm run build # ── Stage 2: Nginx 托管静态资源 ────────────────────────────────────────────── FROM ${NGINX_IMAGE} -RUN apk add --no-cache tzdata \ +ARG APK_MIRROR=mirrors.cloud.tencent.com + +RUN set -eu; \ + if [ "${APK_MIRROR}" != "dl-cdn.alpinelinux.org" ]; then \ + sed -i "s#dl-cdn.alpinelinux.org#${APK_MIRROR}#g" /etc/apk/repositories; \ + fi; \ + apk add --no-cache tzdata \ && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && echo "Asia/Shanghai" > /etc/timezone \ && apk del tzdata diff --git a/manager/deployments/compose/docker-compose.yml b/manager/deployments/compose/docker-compose.yml index b7fd8a216bce2c652c5521feb1421b7a74e738f9..3a656a53c782406cf8a90154d7235afa53f62337 100644 --- a/manager/deployments/compose/docker-compose.yml +++ b/manager/deployments/compose/docker-compose.yml @@ -16,7 +16,7 @@ # # 前置条件: # 1. 已执行 cp config/env.example config/.env 并按需修改 -# 2. Docker 20.10+、Docker Compose v2、Go 1.24+(用于编译后端) +# 2. Docker 20.10+、Docker Compose v2、Go 1.25+(用于编译后端) # # 默认访问入口:http://localhost:13070 (admin;初始密码请通过 config/.env 的 # ADMIN_INIT_PASSWORD 设置,未设置时为公开默认值,请登录后立即修改,见 SECURITY.md) @@ -30,7 +30,9 @@ services: dockerfile: backend/Dockerfile.backend args: SERVICE: footstone - RUNTIME_IMAGE: ${RUNTIME_IMAGE:-mirror.ccs.tencentyun.com/library/alpine:3.19} + # 依赖删除预览使用真实 dnf/repoquery,并要求与目标 OC9 环境一致。 + RUNTIME_IMAGE: ${FOOTSTONE_RUNTIME_IMAGE:-docker.io/opencloudos/opencloudos9-minimal:9.6-v20260714} + APK_MIRROR: ${APK_MIRROR:-mirrors.cloud.tencent.com} image: oc-footstone:${FOOTSTONE_VERSION:-latest} container_name: oc-footstone restart: unless-stopped @@ -58,6 +60,51 @@ services: FOOTSTONE: ip://footstone:17778 # msg-etl tRPC 服务地址(供 footstone 查询指标数据) MSG_ETL: ip://msg-etl:7778 + # 管理面只保留临时控制目录,不挂载或持久化 RPM。发布地址来自 Repository Storage Node 快照。 + REPOSITORY_DATA_ROOT: /var/lib/ocmanager/repository-control + REPOSITORY_PUBLIC_BASE_URL: "" + TMS: http://ocm-api:8080 + REPOSITORY_TMS_USER_NAME: "${REPOSITORY_TMS_USER_NAME:-ocmanager-repository}" + # Footstone 直连 tms/api 时按相同 canonical 规范签名,保持 STRICT=true。 + TMS_API_GW_HMAC_SECRET: "${TMS_API_GW_HMAC_SECRET:?export TMS_API_GW_HMAC_SECRET before starting footstone}" + REPOSITORY_MAX_UPLOAD_BYTES: "${REPOSITORY_MAX_UPLOAD_BYTES:-2147483648}" + REPOSITORY_MAX_CHUNK_BYTES: "${REPOSITORY_MAX_CHUNK_BYTES:-4194304}" + REPOSITORY_MAX_ARCHIVE_FILES: "${REPOSITORY_MAX_ARCHIVE_FILES:-1000}" + REPOSITORY_MAX_EXPANDED_BYTES: "${REPOSITORY_MAX_EXPANDED_BYTES:-8589934592}" + REPOSITORY_MAX_SYNC_BYTES: "${REPOSITORY_MAX_SYNC_BYTES:-68719476736}" + REPOSITORY_MAX_SYNC_PACKAGES: "${REPOSITORY_MAX_SYNC_PACKAGES:-100000}" + REPOSITORY_FILTERED_SYNC_BYTES: "${REPOSITORY_FILTERED_SYNC_BYTES:-2147483648}" + REPOSITORY_FILTERED_SYNC_PACKAGES: "${REPOSITORY_FILTERED_SYNC_PACKAGES:-1000}" + REPOSITORY_METADATA_RESERVE_BYTES: "${REPOSITORY_METADATA_RESERVE_BYTES:-536870912}" + REPOSITORY_UPLOAD_TTL_HOURS: "${REPOSITORY_UPLOAD_TTL_HOURS:-24}" + REPOSITORY_MINIMUM_FREE_BYTES: "${REPOSITORY_MINIMUM_FREE_BYTES:-1073741824}" + REPOSITORY_NODE_INTERNAL_ENABLED: "${REPOSITORY_NODE_INTERNAL_ENABLED:-false}" + REPOSITORY_NODE_INTERNAL_LISTEN_ADDRESS: "0.0.0.0:19443" + REPOSITORY_NODE_INTERNAL_PUBLIC_TARGET: "${REPOSITORY_NODE_INTERNAL_PUBLIC_TARGET:-}" + REPOSITORY_NODE_INTERNAL_SERVER_CERT: /etc/ocmanager/repository-node-pki/server.crt + REPOSITORY_NODE_INTERNAL_SERVER_KEY: /etc/ocmanager/repository-node-pki/server.key + REPOSITORY_NODE_INTERNAL_SERVER_CA: /etc/ocmanager/repository-node-pki/ca.crt + REPOSITORY_NODE_INTERNAL_SERVER_NAME: "${REPOSITORY_NODE_INTERNAL_SERVER_NAME:-}" + REPOSITORY_NODE_INTERNAL_CLIENT_CA: /etc/ocmanager/repository-node-pki/ca.crt + REPOSITORY_NODE_BOOTSTRAP_ENABLED: "${REPOSITORY_NODE_BOOTSTRAP_ENABLED:-false}" + REPOSITORY_NODE_BOOTSTRAP_LISTEN_ADDRESS: "0.0.0.0:19444" + REPOSITORY_NODE_BOOTSTRAP_PUBLIC_TARGET: "${REPOSITORY_NODE_BOOTSTRAP_PUBLIC_TARGET:-}" + REPOSITORY_NODE_BOOTSTRAP_SERVER_CERT: /etc/ocmanager/repository-node-pki/server.crt + REPOSITORY_NODE_BOOTSTRAP_SERVER_KEY: /etc/ocmanager/repository-node-pki/server.key + REPOSITORY_NODE_BOOTSTRAP_SERVER_CA: /etc/ocmanager/repository-node-pki/ca.crt + REPOSITORY_NODE_BOOTSTRAP_SERVER_NAME: "${REPOSITORY_NODE_BOOTSTRAP_SERVER_NAME:-}" + REPOSITORY_NODE_SIGNING_CA_CERT: /etc/ocmanager/repository-node-signing/ca.crt + REPOSITORY_NODE_SIGNING_CA_KEY: /etc/ocmanager/repository-node-signing/ca.key + REPOSITORY_NODE_CERTIFICATE_DAYS: "${REPOSITORY_NODE_CERTIFICATE_DAYS:-90}" + REPOSITORY_NODE_RENEW_BEFORE_DAYS: "${REPOSITORY_NODE_RENEW_BEFORE_DAYS:-30}" + REPOSITORY_NODE_CERTIFICATE_GRACE_HOURS: "${REPOSITORY_NODE_CERTIFICATE_GRACE_HOURS:-24}" + REPOSITORY_NODE_ENROLLMENT_TTL_MINUTES: "${REPOSITORY_NODE_ENROLLMENT_TTL_MINUTES:-15}" + REPOSITORY_NODE_ARTIFACT_ROOT: /var/lib/ocmanager/repository-node-artifacts + REPOSITORY_NODE_BOOTSTRAP_URL: "" + REPOSITORY_NODE_BOOTSTRAP_TOKEN_FILE: "" + REPOSITORY_NODE_ENROLLMENT_ID: "" + REPOSITORY_NODE_INSTALL_TASK_ID: "" + REPOSITORY_NODE_TMS_AGENT_ID: "0" # 当外部 Kafka 的 advertised.listeners 使用了容器名/主机名(而非公网 IP)时, # 在 .env 中设置 FOOTSTONE_KAFKA_EXTRA_HOST=: extra_hosts: @@ -70,12 +117,17 @@ services: # 供本机调试;容器间内部调用走 docker 网络 ip://footstone:17778,不受影响。 # 如需跨机访问 tRPC,请通过带鉴权的前置网关代理,切勿直接对外暴露。 - "127.0.0.1:${FOOTSTONE_TRPC_PORT:-17778}:17778" + - "${REPOSITORY_NODE_INTERNAL_PORT:-19443}:19443" + - "${REPOSITORY_NODE_BOOTSTRAP_PORT:-19444}:19444" volumes: - ${LOG_DIR:-./logs}/footstone:/app/logs + - ${REPOSITORY_NODE_PKI_HOST_DIR:-/etc/ocmanager/repository-node-pki}:/etc/ocmanager/repository-node-pki:ro + - ${REPOSITORY_NODE_SIGNING_PKI_HOST_DIR:-/etc/ocmanager/repository-node-signing}:/etc/ocmanager/repository-node-signing:ro + - ${REPOSITORY_NODE_ARTIFACT_HOST_DIR:-./repository-node-artifacts}:/var/lib/ocmanager/repository-node-artifacts:ro # 存活探针走独立 no-protocol 端口,避免被业务 tRPC/HTTP 路由解析。 - # wget 随 alpine 自带,--spider 仅做 HEAD/GET 探测不下载 body,与 infra 层 clickhouse 一致 + # 同时兼容 OpenCloudOS 与 Alpine 运行时,探针只读取健康状态,不访问业务接口。 healthcheck: - test: ["CMD-SHELL", "wget --spider -q http://127.0.0.1:20001/health || exit 1"] + test: ["CMD-SHELL", "if command -v curl >/dev/null 2>&1; then curl -fsS http://127.0.0.1:20001/health >/dev/null; else wget --spider -q http://127.0.0.1:20001/health; fi"] interval: 15s timeout: 5s retries: 3 @@ -89,7 +141,8 @@ services: dockerfile: backend/Dockerfile.backend args: SERVICE: msg-etl - RUNTIME_IMAGE: ${RUNTIME_IMAGE:-mirror.ccs.tencentyun.com/library/alpine:3.19} + RUNTIME_IMAGE: ${RUNTIME_IMAGE:-docker.io/library/alpine:3.19} + APK_MIRROR: ${APK_MIRROR:-mirrors.cloud.tencent.com} image: oc-msg-etl:${MSG_ETL_VERSION:-latest} container_name: oc-msg-etl restart: unless-stopped @@ -136,7 +189,8 @@ services: dockerfile: backend/Dockerfile.backend args: SERVICE: sysdiagnose - RUNTIME_IMAGE: ${RUNTIME_IMAGE:-mirror.ccs.tencentyun.com/library/alpine:3.19} + RUNTIME_IMAGE: ${RUNTIME_IMAGE:-docker.io/library/alpine:3.19} + APK_MIRROR: ${APK_MIRROR:-mirrors.cloud.tencent.com} image: oc-sysdiagnose:${SYSDIAGNOSE_VERSION:-latest} container_name: oc-sysdiagnose restart: unless-stopped @@ -154,6 +208,9 @@ services: FOOTSTONE: ip://footstone:17778 # 容器内调 tms/api 走 Docker 网络服务名,不能用宿主机 127.0.0.1 TMS: http://ocm-api:8080 + # 与 tms/api、footstone 共用同一网关 HMAC 密钥:sysdiagnose 的 + # /api/v1/ops_tasks/publish 属于状态变更端点,STRICT 模式下必须有签名。 + TMS_API_GW_HMAC_SECRET: "${TMS_API_GW_HMAC_SECRET:?export TMS_API_GW_HMAC_SECRET before starting sysdiagnose}" # 容器内 HTTP/TRPC 固定端口,不使用 .env 中本地二进制方式的 # SYSDIAGNOSE_HTTP_PORT/SYSDIAGNOSE_TRPC_PORT(那两个是宿主机映射/本地端口) SYSDIAGNOSE_HTTP_PORT: "8080" @@ -179,8 +236,10 @@ services: context: ../../frontend dockerfile: ../deployments/compose/Dockerfile.frontend args: - NODE_IMAGE: ${NODE_IMAGE:-mirror.ccs.tencentyun.com/library/node:18-alpine} - NGINX_IMAGE: ${NGINX_IMAGE:-mirror.ccs.tencentyun.com/library/nginx:1.25-alpine} + NODE_IMAGE: ${NODE_IMAGE:-docker.io/library/node:20-alpine} + NGINX_IMAGE: ${NGINX_IMAGE:-docker.io/library/nginx:1.25-alpine} + NPM_REGISTRY: ${NPM_REGISTRY:-https://registry.npmjs.org} + APK_MIRROR: ${APK_MIRROR:-mirrors.cloud.tencent.com} image: oc-frontend:${FRONTEND_VERSION:-latest} container_name: oc-frontend restart: unless-stopped @@ -202,7 +261,7 @@ services: # 默认不启用,避免给最小化部署引入额外组件;详见 docs/runbook.md「监控接入」。 # ────────────────────────────────────────────────────────────────────────── # prometheus: - # image: ${OCM_IMAGE_MIRROR:-mirror.ccs.tencentyun.com}/prom/prometheus:${PROMETHEUS_VERSION:-v2.51.0} + # image: ${OCM_IMAGE_MIRROR:-docker.io}/prom/prometheus:${PROMETHEUS_VERSION:-v2.51.0} # container_name: oc-prometheus # restart: unless-stopped # command: diff --git a/manager/deployments/compose/docker-entrypoint.sh b/manager/deployments/compose/docker-entrypoint.sh index 70af6e7ec8ca4efec15a37fff73104af58a873f0..f3bf98b2e8ecd4d9e1ff1a14b0acadd70b5dc9bf 100755 --- a/manager/deployments/compose/docker-entrypoint.sh +++ b/manager/deployments/compose/docker-entrypoint.sh @@ -27,6 +27,13 @@ case "${SERVICE}" in -trpcconf="${CONF_DIR}/trpc_go.yaml" \ start gateway stream ;; + repository-worker) + exec /app/service \ + -appconf="${CONF_DIR}/application.yaml" \ + -worker-id="repository-worker:${HOSTNAME}" \ + -lease-seconds="${REPOSITORY_WORKER_LEASE_SECONDS:-60}" \ + -poll-milliseconds="${REPOSITORY_WORKER_POLL_MILLISECONDS:-1000}" + ;; msg-etl) exec /app/service \ -appconf="${CONF_DIR}/application.yaml" \ diff --git a/manager/deployments/compose/repository-node.compose.yml b/manager/deployments/compose/repository-node.compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..d3b2415241992e6fe8fee8ebd53a8f6d2c289044 --- /dev/null +++ b/manager/deployments/compose/repository-node.compose.yml @@ -0,0 +1,98 @@ +# 独立 Repository Storage Node。此文件在仓库节点主机上单独运行,不能与管理面 +# docker-compose.yml 合并启动;worker 只通过 mTLS 调用 Footstone,不连接 MySQL。 +name: ocmanager-repository-node + +services: + repository-worker: + build: + context: ../.. + dockerfile: backend/Dockerfile.backend + args: + SERVICE: repository-worker + CONFIG_SERVICE: footstone + RUNTIME_IMAGE: ${REPOSITORY_RUNTIME_IMAGE:-docker.io/opencloudos/opencloudos9-minimal:9.6-v20260714} + image: oc-repository-worker:${REPOSITORY_WORKER_VERSION:-latest} + container_name: oc-repository-worker + restart: unless-stopped + environment: + SERVICE: repository-worker + JWT_SECRET: repository-node-local-only + REPOSITORY_DATA_ROOT: /var/lib/ocmanager/repository-data + REPOSITORY_PUBLIC_BASE_URL: "${REPOSITORY_PUBLIC_BASE_URL:?set the repository node public base URL}" + REPOSITORY_NODE_ID: "${REPOSITORY_NODE_ID:?set the repository storage node ID}" + REPOSITORY_NODE_CONTROL_TARGET: "${REPOSITORY_NODE_CONTROL_TARGET:?set the Footstone mTLS control target as ip://host:port}" + REPOSITORY_NODE_CLIENT_CERT: /etc/ocmanager/repository-node-pki/client.crt + REPOSITORY_NODE_CLIENT_KEY: /etc/ocmanager/repository-node-pki/client.key + REPOSITORY_NODE_SERVER_CA: /etc/ocmanager/repository-node-pki/ca.crt + REPOSITORY_NODE_SERVER_NAME: "${REPOSITORY_NODE_SERVER_NAME:?set the Footstone TLS server name to a certificate SAN}" + REPOSITORY_NODE_HEARTBEAT_SECONDS: "${REPOSITORY_NODE_HEARTBEAT_SECONDS:-10}" + REPOSITORY_NODE_RPC_TIMEOUT_SECONDS: "${REPOSITORY_NODE_RPC_TIMEOUT_SECONDS:-10}" + REPOSITORY_NODE_BOOTSTRAP_URL: "${REPOSITORY_NODE_BOOTSTRAP_URL:-}" + REPOSITORY_NODE_BOOTSTRAP_SERVER_CA: "${REPOSITORY_NODE_BOOTSTRAP_SERVER_CA:-/etc/ocmanager/repository-node-pki/ca.crt}" + REPOSITORY_NODE_BOOTSTRAP_SERVER_NAME: "${REPOSITORY_NODE_BOOTSTRAP_SERVER_NAME:-}" + REPOSITORY_NODE_BOOTSTRAP_TOKEN_FILE: "${REPOSITORY_NODE_BOOTSTRAP_TOKEN_FILE:-/etc/ocmanager/repository-node-bootstrap/token}" + REPOSITORY_NODE_ENROLLMENT_ID: "${REPOSITORY_NODE_ENROLLMENT_ID:-}" + REPOSITORY_NODE_INSTALL_TASK_ID: "${REPOSITORY_NODE_INSTALL_TASK_ID:-}" + REPOSITORY_NODE_TMS_AGENT_ID: "${REPOSITORY_NODE_TMS_AGENT_ID:-0}" + REPOSITORY_NODE_BOOTSTRAP_ENABLED: "false" + REPOSITORY_NODE_BOOTSTRAP_LISTEN_ADDRESS: "" + REPOSITORY_NODE_BOOTSTRAP_SERVER_CERT: "" + REPOSITORY_NODE_BOOTSTRAP_SERVER_KEY: "" + REPOSITORY_NODE_SIGNING_CA_CERT: "" + REPOSITORY_NODE_SIGNING_CA_KEY: "" + REPOSITORY_NODE_ARTIFACT_ROOT: "" + REPOSITORY_NODE_CERTIFICATE_DAYS: "90" + REPOSITORY_NODE_RENEW_BEFORE_DAYS: "30" + REPOSITORY_NODE_CERTIFICATE_GRACE_HOURS: "24" + REPOSITORY_NODE_ENROLLMENT_TTL_MINUTES: "15" + REPOSITORY_NODE_DATA_SERVER_ENABLED: "true" + REPOSITORY_NODE_DATA_SERVER_LISTEN_ADDRESS: "0.0.0.0:19443" + REPOSITORY_NODE_DATA_SERVER_CERT: /etc/ocmanager/repository-node-pki/node-server.crt + REPOSITORY_NODE_DATA_SERVER_KEY: /etc/ocmanager/repository-node-pki/node-server.key + REPOSITORY_NODE_DATA_SERVER_CLIENT_CA: /etc/ocmanager/repository-node-pki/ca.crt + REPOSITORY_NODE_DATA_SERVER_READ_TIMEOUT_SECONDS: "${REPOSITORY_NODE_DATA_SERVER_READ_TIMEOUT_SECONDS:-30}" + REPOSITORY_NODE_MAX_RESULT_BYTES: "${REPOSITORY_NODE_MAX_RESULT_BYTES:-67108864}" + REPOSITORY_MAX_UPLOAD_BYTES: "${REPOSITORY_MAX_UPLOAD_BYTES:-2147483648}" + REPOSITORY_MAX_CHUNK_BYTES: "${REPOSITORY_MAX_CHUNK_BYTES:-4194304}" + REPOSITORY_MAX_ARCHIVE_FILES: "${REPOSITORY_MAX_ARCHIVE_FILES:-1000}" + REPOSITORY_MAX_EXPANDED_BYTES: "${REPOSITORY_MAX_EXPANDED_BYTES:-8589934592}" + REPOSITORY_MAX_SYNC_BYTES: "${REPOSITORY_MAX_SYNC_BYTES:-68719476736}" + REPOSITORY_MAX_SYNC_PACKAGES: "${REPOSITORY_MAX_SYNC_PACKAGES:-100000}" + REPOSITORY_FILTERED_SYNC_BYTES: "${REPOSITORY_FILTERED_SYNC_BYTES:-2147483648}" + REPOSITORY_FILTERED_SYNC_PACKAGES: "${REPOSITORY_FILTERED_SYNC_PACKAGES:-1000}" + REPOSITORY_METADATA_RESERVE_BYTES: "${REPOSITORY_METADATA_RESERVE_BYTES:-536870912}" + REPOSITORY_UPLOAD_TTL_HOURS: "${REPOSITORY_UPLOAD_TTL_HOURS:-24}" + REPOSITORY_MINIMUM_FREE_BYTES: "${REPOSITORY_MINIMUM_FREE_BYTES:-1073741824}" + REPOSITORY_METADATA_SIGNING_KEY_ROOT: /etc/ocmanager/repository-metadata-signing + REPOSITORY_WORKER_LEASE_SECONDS: "${REPOSITORY_WORKER_LEASE_SECONDS:-60}" + REPOSITORY_WORKER_POLL_MILLISECONDS: "${REPOSITORY_WORKER_POLL_MILLISECONDS:-1000}" + ports: + - "${REPOSITORY_NODE_DATA_SERVER_PORT:-19443}:19443" + volumes: + - ${REPOSITORY_DATA_HOST_DIR:-/opt/ocmanager/repository-data}:/var/lib/ocmanager/repository-data:rw + - ${REPOSITORY_NODE_PKI_HOST_DIR:-/etc/ocmanager/repository-node-pki}:/etc/ocmanager/repository-node-pki:rw + - ${REPOSITORY_NODE_BOOTSTRAP_HOST_DIR:-/etc/ocmanager/repository-node-bootstrap}:/etc/ocmanager/repository-node-bootstrap:rw + - ${REPOSITORY_METADATA_SIGNING_KEY_HOST_DIR:-/etc/ocmanager/repository-metadata-signing}:/etc/ocmanager/repository-metadata-signing:ro + - ${LOG_DIR:-./logs}/repository-worker:/app/logs + healthcheck: + test: ["CMD-SHELL", "test -d /var/lib/ocmanager/repository-data/public && test -r /etc/ocmanager/repository-node-pki/ca.crt"] + interval: 15s + timeout: 5s + retries: 3 + start_period: 20s + + repository-publisher: + image: ${NGINX_IMAGE:-docker.io/library/nginx:1.25-alpine} + container_name: oc-repository-publisher + restart: unless-stopped + ports: + - "${REPOSITORY_PUBLIC_PORT:-18090}:80" + volumes: + - ./repository-node.nginx.conf:/etc/nginx/nginx.conf:ro + - ${REPOSITORY_DATA_HOST_DIR:-/opt/ocmanager/repository-data}/public:/var/lib/ocmanager/repository-data/public:ro + healthcheck: + test: ["CMD-SHELL", "wget --spider -q http://127.0.0.1/health || exit 1"] + interval: 15s + timeout: 5s + retries: 3 + start_period: 10s diff --git a/manager/deployments/compose/repository-node.nginx.conf b/manager/deployments/compose/repository-node.nginx.conf new file mode 100644 index 0000000000000000000000000000000000000000..c4c77cfa2aa265e1401f8810f49d9083bffb4573 --- /dev/null +++ b/manager/deployments/compose/repository-node.nginx.conf @@ -0,0 +1,39 @@ +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + access_log /var/log/nginx/access.log; + sendfile on; + + server { + listen 80 default_server; + server_name _; + + location = /health { + access_log off; + default_type text/plain; + return 200 "ok\n"; + } + + location ^~ /repositories/ { + alias /var/lib/ocmanager/repository-data/public/; + autoindex off; + limit_except GET { + deny all; + } + add_header X-Content-Type-Options nosniff always; + } + + location / { + return 404; + } + } +} diff --git a/manager/frontend/Makefile b/manager/frontend/Makefile index 891cafd52cc3485febc0f49c83fb70268ce9e63c..143a74c46172c15795c4f9d07e62b4dc8d0e76f5 100755 --- a/manager/frontend/Makefile +++ b/manager/frontend/Makefile @@ -1,16 +1,16 @@ # Manager 前端:依赖安装与生产构建 -# 需要 Node.js >= 18(Vite 6) +# 需要 Node.js >= 20(当前前端依赖基线) # # make 子进程通常不会 source ~/.bashrc,故可能仍用系统自带的旧 node。 -# 若已通过 nvm 安装 Node 18+,下面会把「最新已安装的 v18.x」的 bin 置于 PATH 最前。 +# 若已通过 nvm 安装 Node 20+,下面会把「最新已安装的 v20.x」的 bin 置于 PATH 最前。 .PHONY: all deps build lint check-node clean NVM_DIR ?= $(HOME)/.nvm -# 例如 .../versions/node/v18.20.8 -NVM18_HOME := $(shell ls -d "$(NVM_DIR)"/versions/node/v18.* 2>/dev/null | sort -V | tail -1) -ifneq ($(NVM18_HOME),) -export PATH := $(NVM18_HOME)/bin:$(PATH) +# 例如 .../versions/node/v20.19.0 +NVM20_HOME := $(shell ls -d "$(NVM_DIR)"/versions/node/v20.* 2>/dev/null | sort -V | tail -1) +ifneq ($(NVM20_HOME),) +export PATH := $(NVM20_HOME)/bin:$(PATH) endif NPM ?= npm @@ -18,7 +18,7 @@ NPM ?= npm all: build check-node: - @node -e 'const m=+process.versions.node.split(".")[0]; if(m<18){console.error("Node.js 18+ required, got "+process.version+"; install nvm Node 18 or set PATH to node>=18"); process.exit(1)}' + @node -e 'const m=+process.versions.node.split(".")[0]; if(m<20){console.error("Node.js 20+ required, got "+process.version+"; install nvm Node 20 or set PATH to node>=20"); process.exit(1)}' deps: check-node $(NPM) install diff --git a/manager/frontend/README-zh_CN.md b/manager/frontend/README-zh_CN.md index b2fe655ac5c65cde2ae7fc4015ae7957b0b17ecf..74774c4085ad50ea23e482af833f9df6cdffefa2 100755 --- a/manager/frontend/README-zh_CN.md +++ b/manager/frontend/README-zh_CN.md @@ -15,7 +15,7 @@ | 模块 | 选型 | |------|------| | 框架 | Vue 3(Composition API) | -| 构建 | Vite 6(需要 Node.js 18+) | +| 构建 | Vite 6(需要 Node.js 20+) | | UI | TDesign for Vue 3 | | 状态 | Pinia | | 路由 | Vue Router 4 | diff --git a/manager/frontend/README.md b/manager/frontend/README.md index 93536962f04df0c09d3934ca9044de16374eba11..d4f89c2e1a7202a37455eb0c770ff653c01f9d8b 100755 --- a/manager/frontend/README.md +++ b/manager/frontend/README.md @@ -14,7 +14,7 @@ application that talks to the backend services in `manager/backend/`. | Area | Choice | |------|--------| | Framework | Vue 3 (Composition API) | -| Build tool | Vite 6 (requires Node.js 18+) | +| Build tool | Vite 6 (requires Node.js 20+) | | UI | TDesign for Vue 3 | | State | Pinia | | Router | Vue Router 4 | diff --git a/manager/frontend/cache.dockerfile b/manager/frontend/cache.dockerfile index 985b1e084029cc49545a1a4ce93f6ac591bb604c..21998eb08e0f1a02d8366481b9eb81c7b2b1e005 100755 --- a/manager/frontend/cache.dockerfile +++ b/manager/frontend/cache.dockerfile @@ -1,5 +1,6 @@ -# 选择一个 Base 镜像 -FROM mirror.ccs.tencentyun.com/library/node:12 +# 可通过 --build-arg NODE_IMAGE=... 切换为内网镜像。 +ARG NODE_IMAGE=docker.io/library/node:20-alpine +FROM ${NODE_IMAGE} # 设置工作目录 WORKDIR /space diff --git a/manager/frontend/deploy/Dockerfile b/manager/frontend/deploy/Dockerfile index 86c6699d508fe64ec8ce06555721681154b362ef..f07bff5be595c1cb7b4e1a4df13393ca5a13595e 100755 --- a/manager/frontend/deploy/Dockerfile +++ b/manager/frontend/deploy/Dockerfile @@ -1,4 +1,5 @@ -FROM mirror.ccs.tencentyun.com/opencloudos/opencloudos9-minimal:latest +ARG RUNTIME_IMAGE=docker.io/opencloudos/opencloudos9-minimal:9.6-v20260714 +FROM ${RUNTIME_IMAGE} ENV ENTRY=/home ENV DEST=/usr/share/nginx/html ENV CONF=/etc/nginx diff --git a/manager/frontend/package-lock.json b/manager/frontend/package-lock.json index 07d2a5e714e2981c90f3e4994a8a1b2ce8588696..9ad85d5f5ff868ea59b11b5499fb9891ab0bff73 100644 --- a/manager/frontend/package-lock.json +++ b/manager/frontend/package-lock.json @@ -39,7 +39,7 @@ "tdesign-vue-next": "1.10.7", "true-myth": "^9.3.1", "tvision-color": "~1.6.0", - "typescript": "^5.7.3", + "typescript": "5.7.3", "uuid": "^11.1.1", "vditor": "^3.10.9", "vite-svg-loader": "^5.1.0", @@ -68,20 +68,21 @@ "less": "^4.1.0", "lint-staged": "^15.4.3", "mockjs": "^1.1.0", + "postcss-html": "1.5.0", + "postcss-less": "6.0.0", "prettier": "^3.5.2", "stylelint": "~16.14.1", - "stylelint-config-prettier": "~9.0.3", - "stylelint-less": "3.0.1", "stylelint-order": "~6.0.4", "terser": "^5.18.2", "vite": "^6.2.4", "vite-plugin-mock": "^3.0.2", "vite-plugin-require-transform": "^1.0.12", "vite-plugin-theme": "^0.8.1", - "vitest": "^4.1.9" + "vitest": "^4.1.9", + "vue-tsc": "2.2.4" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@antfu/install-pkg": { @@ -3700,6 +3701,35 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, "node_modules/@vue/babel-helper-vue-transform-on": { "version": "1.5.0", "resolved": "https://mirrors.tencent.com/npm/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz", @@ -3795,11 +3825,80 @@ "@vue/shared": "3.5.32" } }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, "node_modules/@vue/devtools-api": { "version": "6.6.4", "resolved": "https://mirrors.tencent.com/npm/@vue/devtools-api/-/devtools-api-6.6.4.tgz", "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==" }, + "node_modules/@vue/language-core": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.4.tgz", + "integrity": "sha512-eGGdw7eWUwdIn9Fy/irJ7uavCGfgemuHQABgJ/hU1UgZFnbTg9VWeXvHQdhY+2SPQZWJqWXvRWIg67t4iWEa+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "~2.4.11", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/language-core/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@vue/language-core/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@vue/reactivity": { "version": "3.5.32", "resolved": "https://mirrors.tencent.com/npm/@vue/reactivity/-/reactivity-3.5.32.tgz", @@ -3971,6 +4070,13 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/alien-signals": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", + "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "dev": true, + "license": "MIT" + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://mirrors.tencent.com/npm/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -4556,336 +4662,126 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, - "node_modules/cherry-markdown": { - "version": "0.11.1", - "resolved": "https://mirrors.tencent.com/npm/cherry-markdown/-/cherry-markdown-0.11.1.tgz", - "integrity": "sha512-s9wsRQJOIYoUT7sjMUWfjUJIGmmlRxo2nqO4m2yx/8JvvalihTu9ksTANZ1XQml2CWYF2VocMB+rx4MErpnm+A==", + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://mirrors.tencent.com/npm/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, "dependencies": { - "@codemirror/autocomplete": "^6.18.6", - "@codemirror/commands": "^6.8.1", - "@codemirror/lang-markdown": "^6.3.2", - "@codemirror/language": "^6.12.2", - "@codemirror/search": "^6.5.10", - "@codemirror/state": "^6.5.2", - "@codemirror/view": "^6.40.0", - "@lezer/highlight": "^1.2.1", - "@replit/codemirror-vim": "^6.3.0", - "@types/codemirror": "^0.0.108", - "crypto-js": "^4.2.0", - "dompurify": "^3.2.6", - "htmlparser2": "^10.0.0", - "jsdom": "~19.0.0", - "ws": "^8.18.0" + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" }, "engines": { - "node": ">=24" + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" }, "optionalDependencies": { - "mermaid": "^11.14.0" + "fsevents": "~2.3.2" } }, - "node_modules/cherry-markdown/node_modules/data-urls": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", - "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", - "license": "MIT", + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://mirrors.tencent.com/npm/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", "dependencies": { - "abab": "^2.0.6", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0" + "is-glob": "^4.0.1" }, "engines": { - "node": ">=12" + "node": ">= 6" } }, - "node_modules/cherry-markdown/node_modules/data-urls/node_modules/whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", - "license": "MIT", + "node_modules/chroma-js": { + "version": "2.1.2", + "resolved": "https://mirrors.tencent.com/npm/chroma-js/-/chroma-js-2.1.2.tgz", + "integrity": "sha512-ri/ouYDWuxfus3UcaMxC1Tfp3IE9K5iQzxc2hSxbBRVNQFut1UuGAsZmiAf2mOUubzGJwgMSv9lHg+XqLaz1QQ==", + "license": "(BSD-3-Clause AND Apache-2.0)", "dependencies": { - "tr46": "^3.0.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" + "cross-env": "^6.0.3" } }, - "node_modules/cherry-markdown/node_modules/html-encoding-sniffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", - "license": "MIT", + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://mirrors.tencent.com/npm/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", "dependencies": { - "whatwg-encoding": "^2.0.0" + "clsx": "^2.1.1" }, - "engines": { - "node": ">=12" + "funding": { + "url": "https://polar.sh/cva" } }, - "node_modules/cherry-markdown/node_modules/jsdom": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-19.0.0.tgz", - "integrity": "sha512-RYAyjCbxy/vri/CfnjUWJQQtZ3LKlLnDqj+9XLNnJPgEGeirZs3hllKR20re8LUZ6o1b1X4Jat+Qd26zmP41+A==", - "license": "MIT", + "node_modules/clean-css": { + "version": "5.3.3", + "resolved": "https://mirrors.tencent.com/npm/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "dev": true, "dependencies": { - "abab": "^2.0.5", - "acorn": "^8.5.0", - "acorn-globals": "^6.0.0", - "cssom": "^0.5.0", - "cssstyle": "^2.3.0", - "data-urls": "^3.0.1", - "decimal.js": "^10.3.1", - "domexception": "^4.0.0", - "escodegen": "^2.0.0", - "form-data": "^4.0.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.0", - "parse5": "6.0.1", - "saxes": "^5.0.1", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.0.0", - "w3c-hr-time": "^1.0.2", - "w3c-xmlserializer": "^3.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^10.0.0", - "ws": "^8.2.3", - "xml-name-validator": "^4.0.0" + "source-map": "~0.6.0" }, "engines": { - "node": ">=12" - }, - "peerDependencies": { - "canvas": "^2.5.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } + "node": ">= 10.0" } }, - "node_modules/cherry-markdown/node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "license": "MIT" - }, - "node_modules/cherry-markdown/node_modules/saxes": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", - "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", - "license": "ISC", + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://mirrors.tencent.com/npm/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, "dependencies": { - "xmlchars": "^2.2.0" + "restore-cursor": "^3.1.0" }, "engines": { - "node": ">=10" + "node": ">=8" } }, - "node_modules/cherry-markdown/node_modules/tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", - "license": "BSD-3-Clause", - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://mirrors.tencent.com/npm/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, "engines": { "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cherry-markdown/node_modules/tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", - "license": "MIT", + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://mirrors.tencent.com/npm/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, "dependencies": { - "punycode": "^2.1.1" + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cherry-markdown/node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://mirrors.tencent.com/npm/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/cherry-markdown/node_modules/w3c-xmlserializer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz", - "integrity": "sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg==", - "license": "MIT", - "dependencies": { - "xml-name-validator": "^4.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cherry-markdown/node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/cherry-markdown/node_modules/whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/cherry-markdown/node_modules/whatwg-url": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-10.0.0.tgz", - "integrity": "sha512-CLxxCmdUby142H5FZzn4D8ikO1cmypvXVQktsgosNy4a4BHrDHeciBBGZhb0bNoR5/MltoCatso+vFjjGx8t0w==", - "license": "MIT", - "dependencies": { - "tr46": "^3.0.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://mirrors.tencent.com/npm/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://mirrors.tencent.com/npm/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/chroma-js": { - "version": "2.1.2", - "resolved": "https://mirrors.tencent.com/npm/chroma-js/-/chroma-js-2.1.2.tgz", - "integrity": "sha512-ri/ouYDWuxfus3UcaMxC1Tfp3IE9K5iQzxc2hSxbBRVNQFut1UuGAsZmiAf2mOUubzGJwgMSv9lHg+XqLaz1QQ==", - "license": "(BSD-3-Clause AND Apache-2.0)", - "dependencies": { - "cross-env": "^6.0.3" - } - }, - "node_modules/class-variance-authority": { - "version": "0.7.1", - "resolved": "https://mirrors.tencent.com/npm/class-variance-authority/-/class-variance-authority-0.7.1.tgz", - "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", - "dependencies": { - "clsx": "^2.1.1" - }, - "funding": { - "url": "https://polar.sh/cva" - } - }, - "node_modules/clean-css": { - "version": "5.3.3", - "resolved": "https://mirrors.tencent.com/npm/clean-css/-/clean-css-5.3.3.tgz", - "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", - "dev": true, - "dependencies": { - "source-map": "~0.6.0" - }, - "engines": { - "node": ">= 10.0" - } - }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://mirrors.tencent.com/npm/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://mirrors.tencent.com/npm/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate": { - "version": "4.0.0", - "resolved": "https://mirrors.tencent.com/npm/cli-truncate/-/cli-truncate-4.0.0.tgz", - "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", - "dev": true, - "dependencies": { - "slice-ansi": "^5.0.0", - "string-width": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://mirrors.tencent.com/npm/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/cli-truncate/node_modules/emoji-regex": { @@ -6230,6 +6126,13 @@ "resolved": "https://mirrors.tencent.com/npm/dayjs/-/dayjs-1.11.20.tgz", "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==" }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://mirrors.tencent.com/npm/debug/-/debug-4.4.3.tgz", @@ -8486,6 +8389,16 @@ "node": ">= 0.4" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, "node_modules/highlight.js": { "version": "11.11.1", "resolved": "https://mirrors.tencent.com/npm/highlight.js/-/highlight.js-11.11.1.tgz", @@ -10741,6 +10654,13 @@ "resolved": "https://mirrors.tencent.com/npm/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, "node_modules/mute-stream": { "version": "0.0.8", "resolved": "https://mirrors.tencent.com/npm/mute-stream/-/mute-stream-0.0.8.tgz", @@ -11428,6 +11348,13 @@ "node": ">= 0.8" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, "node_modules/path-data-parser": { "version": "0.1.0", "resolved": "https://mirrors.tencent.com/npm/path-data-parser/-/path-data-parser-0.1.0.tgz", @@ -11651,6 +11578,92 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-html": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/postcss-html/-/postcss-html-1.5.0.tgz", + "integrity": "sha512-kCMRWJRHKicpA166kc2lAVUGxDZL324bkj/pVOb6RhjB0Z5Krl7mN0AsVkBhVIRZZirY0lyQXG38HCVaoKVNoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "htmlparser2": "^8.0.0", + "js-tokens": "^8.0.0", + "postcss": "^8.4.0", + "postcss-safe-parser": "^6.0.0" + }, + "engines": { + "node": "^12 || >=14" + } + }, + "node_modules/postcss-html/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/postcss-html/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/postcss-html/node_modules/js-tokens": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.3.tgz", + "integrity": "sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-html/node_modules/postcss-safe-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", + "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, + "node_modules/postcss-less": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-less/-/postcss-less-6.0.0.tgz", + "integrity": "sha512-FPX16mQLyEjLzEuuJtxA8X3ejDLNGGEG503d2YGZR5Ask1SpDN8KmZUMpzCvyalWRywAn1n1VOA5dcqfCLo5rg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "postcss": "^8.3.5" + } + }, "node_modules/postcss-resolve-nested-selector": { "version": "0.1.6", "resolved": "https://mirrors.tencent.com/npm/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.6.tgz", @@ -12981,36 +12994,6 @@ "node": ">=18.12.0" } }, - "node_modules/stylelint-config-prettier": { - "version": "9.0.5", - "resolved": "https://mirrors.tencent.com/npm/stylelint-config-prettier/-/stylelint-config-prettier-9.0.5.tgz", - "integrity": "sha512-U44lELgLZhbAD/xy/vncZ2Pq8sh2TnpiPvo38Ifg9+zeioR+LAkHu0i6YORIOxFafZoVg0xqQwex6e6F25S5XA==", - "dev": true, - "bin": { - "stylelint-config-prettier": "bin/check.js", - "stylelint-config-prettier-check": "bin/check.js" - }, - "engines": { - "node": ">= 12" - }, - "peerDependencies": { - "stylelint": ">= 11.x < 15" - } - }, - "node_modules/stylelint-less": { - "version": "3.0.1", - "resolved": "https://mirrors.tencent.com/npm/stylelint-less/-/stylelint-less-3.0.1.tgz", - "integrity": "sha512-6GkZ4jhmReXxX61IiNaniZFuyTzYTTC4HvRLNNok883d1ux/wUodM1uik+iAHZM1VSCwNASaj0Th6ZX46WZLMw==", - "dev": true, - "dependencies": { - "postcss-resolve-nested-selector": "^0.1.1", - "postcss-value-parser": "4.2.0" - }, - "peerDependencies": { - "postcss": "^8.4.31", - "stylelint": "^16.0.2" - } - }, "node_modules/stylelint-order": { "version": "6.0.4", "resolved": "https://mirrors.tencent.com/npm/stylelint-order/-/stylelint-order-6.0.4.tgz", @@ -13425,9 +13408,220 @@ "zod": "^3.23.8" } }, - "node_modules/tdesign-web-components/node_modules/zod": { - "version": "3.25.76", - "resolved": "https://mirrors.tencent.com/npm/zod/-/zod-3.25.76.tgz", + "node_modules/tdesign-web-components/node_modules/cherry-markdown": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/cherry-markdown/-/cherry-markdown-0.11.0.tgz", + "integrity": "sha512-2NkYAiBsY/2wlIM5/moFsXuBPMLvrTWRPUXO44Gp2iod6eQ3Wz+J47jaq8tvH4w1F5dUnc02ETFwbtiCByGuEQ==", + "license": "Apache-2.0", + "dependencies": { + "@codemirror/autocomplete": "^6.18.6", + "@codemirror/commands": "^6.8.1", + "@codemirror/lang-markdown": "^6.3.2", + "@codemirror/language": "^6.12.2", + "@codemirror/search": "^6.5.10", + "@codemirror/state": "^6.5.2", + "@codemirror/view": "^6.40.0", + "@lezer/highlight": "^1.2.1", + "@replit/codemirror-vim": "^6.3.0", + "@types/codemirror": "^0.0.108", + "crypto-js": "^4.2.0", + "dompurify": "^3.2.6", + "htmlparser2": "^10.0.0", + "jsdom": "~19.0.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "mermaid": "^11.14.0" + } + }, + "node_modules/tdesign-web-components/node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/tdesign-web-components/node_modules/data-urls/node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/tdesign-web-components/node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/tdesign-web-components/node_modules/jsdom": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-19.0.0.tgz", + "integrity": "sha512-RYAyjCbxy/vri/CfnjUWJQQtZ3LKlLnDqj+9XLNnJPgEGeirZs3hllKR20re8LUZ6o1b1X4Jat+Qd26zmP41+A==", + "license": "MIT", + "dependencies": { + "abab": "^2.0.5", + "acorn": "^8.5.0", + "acorn-globals": "^6.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.1", + "decimal.js": "^10.3.1", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.0", + "parse5": "6.0.1", + "saxes": "^5.0.1", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.0.0", + "w3c-hr-time": "^1.0.2", + "w3c-xmlserializer": "^3.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^10.0.0", + "ws": "^8.2.3", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/tdesign-web-components/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "license": "MIT" + }, + "node_modules/tdesign-web-components/node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tdesign-web-components/node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tdesign-web-components/node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/tdesign-web-components/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/tdesign-web-components/node_modules/w3c-xmlserializer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz", + "integrity": "sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg==", + "license": "MIT", + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/tdesign-web-components/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/tdesign-web-components/node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/tdesign-web-components/node_modules/whatwg-url": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-10.0.0.tgz", + "integrity": "sha512-CLxxCmdUby142H5FZzn4D8ikO1cmypvXVQktsgosNy4a4BHrDHeciBBGZhb0bNoR5/MltoCatso+vFjjGx8t0w==", + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/tdesign-web-components/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://mirrors.tencent.com/npm/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -13821,9 +14015,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://mirrors.tencent.com/npm/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -14356,6 +14550,13 @@ "node": ">=0.10.0" } }, + "node_modules/vscode-uri": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.2.0.tgz", + "integrity": "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==", + "dev": true, + "license": "MIT" + }, "node_modules/vue": { "version": "3.5.32", "resolved": "https://mirrors.tencent.com/npm/vue/-/vue-3.5.32.tgz", @@ -14488,6 +14689,23 @@ "vue": "^3.5.0" } }, + "node_modules/vue-tsc": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.4.tgz", + "integrity": "sha512-3EVHlxtpMXcb5bCaK7QDFTbEkMusDfVk0HVRrkv5hEb+Clpu9a96lKUXJAeD/akRlkoA4H8MCHgBDN19S6FnzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "~2.4.11", + "@vue/language-core": "2.2.4" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", @@ -17498,6 +17716,32 @@ "tinyrainbow": "^3.1.0" } }, + "@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "dev": true, + "requires": { + "@volar/source-map": "2.4.28" + } + }, + "@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true + }, + "@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "dev": true, + "requires": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, "@vue/babel-helper-vue-transform-on": { "version": "1.5.0", "resolved": "https://mirrors.tencent.com/npm/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz", @@ -17577,11 +17821,63 @@ "@vue/shared": "3.5.32" } }, + "@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "requires": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, "@vue/devtools-api": { "version": "6.6.4", "resolved": "https://mirrors.tencent.com/npm/@vue/devtools-api/-/devtools-api-6.6.4.tgz", "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==" }, + "@vue/language-core": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.4.tgz", + "integrity": "sha512-eGGdw7eWUwdIn9Fy/irJ7uavCGfgemuHQABgJ/hU1UgZFnbTg9VWeXvHQdhY+2SPQZWJqWXvRWIg67t4iWEa+Q==", + "dev": true, + "requires": { + "@volar/language-core": "~2.4.11", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "dependencies": { + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.2" + } + } + } + }, "@vue/reactivity": { "version": "3.5.32", "resolved": "https://mirrors.tencent.com/npm/@vue/reactivity/-/reactivity-3.5.32.tgz", @@ -17702,6 +17998,12 @@ "require-from-string": "^2.0.2" } }, + "alien-signals": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", + "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "dev": true + }, "ansi-escapes": { "version": "4.3.2", "resolved": "https://mirrors.tencent.com/npm/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -18002,236 +18304,84 @@ "keyv": "^5.6.0", "qified": "^0.9.0" }, - "dependencies": { - "keyv": { - "version": "5.6.0", - "resolved": "https://mirrors.tencent.com/npm/keyv/-/keyv-5.6.0.tgz", - "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", - "dev": true, - "requires": { - "@keyv/serialize": "^1.1.1" - } - } - } - }, - "cachedir": { - "version": "2.3.0", - "resolved": "https://mirrors.tencent.com/npm/cachedir/-/cachedir-2.3.0.tgz", - "integrity": "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==", - "dev": true - }, - "call-bind": { - "version": "1.0.8", - "resolved": "https://mirrors.tencent.com/npm/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, - "requires": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - } - }, - "call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://mirrors.tencent.com/npm/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "requires": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - } - }, - "call-bound": { - "version": "1.0.4", - "resolved": "https://mirrors.tencent.com/npm/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "requires": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - } - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://mirrors.tencent.com/npm/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "caniuse-lite": { - "version": "1.0.30001806", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", - "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==" - }, - "chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "dev": true - }, - "chalk": { - "version": "5.6.2", - "resolved": "https://mirrors.tencent.com/npm/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true - }, - "chardet": { - "version": "0.7.0", - "resolved": "https://mirrors.tencent.com/npm/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true - }, - "cherry-markdown": { - "version": "0.11.1", - "resolved": "https://mirrors.tencent.com/npm/cherry-markdown/-/cherry-markdown-0.11.1.tgz", - "integrity": "sha512-s9wsRQJOIYoUT7sjMUWfjUJIGmmlRxo2nqO4m2yx/8JvvalihTu9ksTANZ1XQml2CWYF2VocMB+rx4MErpnm+A==", - "requires": { - "@codemirror/autocomplete": "^6.18.6", - "@codemirror/commands": "^6.8.1", - "@codemirror/lang-markdown": "^6.3.2", - "@codemirror/language": "^6.12.2", - "@codemirror/search": "^6.5.10", - "@codemirror/state": "^6.5.2", - "@codemirror/view": "^6.40.0", - "@lezer/highlight": "^1.2.1", - "@replit/codemirror-vim": "^6.3.0", - "@types/codemirror": "^0.0.108", - "crypto-js": "^4.2.0", - "dompurify": "^3.2.6", - "htmlparser2": "^10.0.0", - "jsdom": "~19.0.0", - "mermaid": "^11.14.0", - "ws": "^8.18.0" - }, - "dependencies": { - "data-urls": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", - "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", - "requires": { - "abab": "^2.0.6", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0" - }, - "dependencies": { - "whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", - "requires": { - "tr46": "^3.0.0", - "webidl-conversions": "^7.0.0" - } - } - } - }, - "html-encoding-sniffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", - "requires": { - "whatwg-encoding": "^2.0.0" - } - }, - "jsdom": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-19.0.0.tgz", - "integrity": "sha512-RYAyjCbxy/vri/CfnjUWJQQtZ3LKlLnDqj+9XLNnJPgEGeirZs3hllKR20re8LUZ6o1b1X4Jat+Qd26zmP41+A==", - "requires": { - "abab": "^2.0.5", - "acorn": "^8.5.0", - "acorn-globals": "^6.0.0", - "cssom": "^0.5.0", - "cssstyle": "^2.3.0", - "data-urls": "^3.0.1", - "decimal.js": "^10.3.1", - "domexception": "^4.0.0", - "escodegen": "^2.0.0", - "form-data": "^4.0.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.0", - "parse5": "6.0.1", - "saxes": "^5.0.1", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.0.0", - "w3c-hr-time": "^1.0.2", - "w3c-xmlserializer": "^3.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^10.0.0", - "ws": "^8.2.3", - "xml-name-validator": "^4.0.0" - } - }, - "parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" - }, - "saxes": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", - "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", - "requires": { - "xmlchars": "^2.2.0" - } - }, - "tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", - "requires": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - } - }, - "tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", - "requires": { - "punycode": "^2.1.1" - } - }, - "universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==" - }, - "w3c-xmlserializer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz", - "integrity": "sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg==", - "requires": { - "xml-name-validator": "^4.0.0" - } - }, - "webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" - }, - "whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==" - }, - "whatwg-url": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-10.0.0.tgz", - "integrity": "sha512-CLxxCmdUby142H5FZzn4D8ikO1cmypvXVQktsgosNy4a4BHrDHeciBBGZhb0bNoR5/MltoCatso+vFjjGx8t0w==", + "dependencies": { + "keyv": { + "version": "5.6.0", + "resolved": "https://mirrors.tencent.com/npm/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, "requires": { - "tr46": "^3.0.0", - "webidl-conversions": "^7.0.0" + "@keyv/serialize": "^1.1.1" } } } }, + "cachedir": { + "version": "2.3.0", + "resolved": "https://mirrors.tencent.com/npm/cachedir/-/cachedir-2.3.0.tgz", + "integrity": "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==", + "dev": true + }, + "call-bind": { + "version": "1.0.8", + "resolved": "https://mirrors.tencent.com/npm/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + } + }, + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://mirrors.tencent.com/npm/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, + "call-bound": { + "version": "1.0.4", + "resolved": "https://mirrors.tencent.com/npm/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://mirrors.tencent.com/npm/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==" + }, + "chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true + }, + "chalk": { + "version": "5.6.2", + "resolved": "https://mirrors.tencent.com/npm/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://mirrors.tencent.com/npm/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, "chokidar": { "version": "3.6.0", "resolved": "https://mirrors.tencent.com/npm/chokidar/-/chokidar-3.6.0.tgz", @@ -19320,6 +19470,12 @@ "resolved": "https://mirrors.tencent.com/npm/dayjs/-/dayjs-1.11.20.tgz", "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==" }, + "de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true + }, "debug": { "version": "4.4.3", "resolved": "https://mirrors.tencent.com/npm/debug/-/debug-4.4.3.tgz", @@ -20929,6 +21085,12 @@ "function-bind": "^1.1.2" } }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, "highlight.js": { "version": "11.11.1", "resolved": "https://mirrors.tencent.com/npm/highlight.js/-/highlight.js-11.11.1.tgz", @@ -22466,6 +22628,12 @@ "resolved": "https://mirrors.tencent.com/npm/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true + }, "mute-stream": { "version": "0.0.8", "resolved": "https://mirrors.tencent.com/npm/mute-stream/-/mute-stream-0.0.8.tgz", @@ -22943,6 +23111,12 @@ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "dev": true }, + "path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, "path-data-parser": { "version": "0.1.0", "resolved": "https://mirrors.tencent.com/npm/path-data-parser/-/path-data-parser-0.1.0.tgz", @@ -23086,6 +23260,56 @@ "source-map-js": "^1.2.1" } }, + "postcss-html": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/postcss-html/-/postcss-html-1.5.0.tgz", + "integrity": "sha512-kCMRWJRHKicpA166kc2lAVUGxDZL324bkj/pVOb6RhjB0Z5Krl7mN0AsVkBhVIRZZirY0lyQXG38HCVaoKVNoA==", + "dev": true, + "requires": { + "htmlparser2": "^8.0.0", + "js-tokens": "^8.0.0", + "postcss": "^8.4.0", + "postcss-safe-parser": "^6.0.0" + }, + "dependencies": { + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true + }, + "htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "js-tokens": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.3.tgz", + "integrity": "sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==", + "dev": true + }, + "postcss-safe-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", + "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", + "dev": true + } + } + }, + "postcss-less": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-less/-/postcss-less-6.0.0.tgz", + "integrity": "sha512-FPX16mQLyEjLzEuuJtxA8X3ejDLNGGEG503d2YGZR5Ask1SpDN8KmZUMpzCvyalWRywAn1n1VOA5dcqfCLo5rg==", + "dev": true + }, "postcss-resolve-nested-selector": { "version": "0.1.6", "resolved": "https://mirrors.tencent.com/npm/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.6.tgz", @@ -24085,22 +24309,6 @@ } } }, - "stylelint-config-prettier": { - "version": "9.0.5", - "resolved": "https://mirrors.tencent.com/npm/stylelint-config-prettier/-/stylelint-config-prettier-9.0.5.tgz", - "integrity": "sha512-U44lELgLZhbAD/xy/vncZ2Pq8sh2TnpiPvo38Ifg9+zeioR+LAkHu0i6YORIOxFafZoVg0xqQwex6e6F25S5XA==", - "dev": true - }, - "stylelint-less": { - "version": "3.0.1", - "resolved": "https://mirrors.tencent.com/npm/stylelint-less/-/stylelint-less-3.0.1.tgz", - "integrity": "sha512-6GkZ4jhmReXxX61IiNaniZFuyTzYTTC4HvRLNNok883d1ux/wUodM1uik+iAHZM1VSCwNASaj0Th6ZX46WZLMw==", - "dev": true, - "requires": { - "postcss-resolve-nested-selector": "^0.1.1", - "postcss-value-parser": "4.2.0" - } - }, "stylelint-order": { "version": "6.0.4", "resolved": "https://mirrors.tencent.com/npm/stylelint-order/-/stylelint-order-6.0.4.tgz", @@ -24314,7 +24522,7 @@ "requires": { "@babel/runtime": "^7.24.7", "@popperjs/core": "^2.11.8", - "cherry-markdown": "^0.11.0-alpha-5", + "cherry-markdown": "0.11.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "copy-to-clipboard": "^3.3.3", @@ -24332,6 +24540,156 @@ "zod": "^3.23.8" }, "dependencies": { + "cherry-markdown": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/cherry-markdown/-/cherry-markdown-0.11.0.tgz", + "integrity": "sha512-2NkYAiBsY/2wlIM5/moFsXuBPMLvrTWRPUXO44Gp2iod6eQ3Wz+J47jaq8tvH4w1F5dUnc02ETFwbtiCByGuEQ==", + "requires": { + "@codemirror/autocomplete": "^6.18.6", + "@codemirror/commands": "^6.8.1", + "@codemirror/lang-markdown": "^6.3.2", + "@codemirror/language": "^6.12.2", + "@codemirror/search": "^6.5.10", + "@codemirror/state": "^6.5.2", + "@codemirror/view": "^6.40.0", + "@lezer/highlight": "^1.2.1", + "@replit/codemirror-vim": "^6.3.0", + "@types/codemirror": "^0.0.108", + "crypto-js": "^4.2.0", + "dompurify": "^3.2.6", + "htmlparser2": "^10.0.0", + "jsdom": "~19.0.0", + "mermaid": "^11.14.0", + "ws": "^8.18.0" + } + }, + "data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "requires": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "dependencies": { + "whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "requires": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + } + } + } + }, + "html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "requires": { + "whatwg-encoding": "^2.0.0" + } + }, + "jsdom": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-19.0.0.tgz", + "integrity": "sha512-RYAyjCbxy/vri/CfnjUWJQQtZ3LKlLnDqj+9XLNnJPgEGeirZs3hllKR20re8LUZ6o1b1X4Jat+Qd26zmP41+A==", + "requires": { + "abab": "^2.0.5", + "acorn": "^8.5.0", + "acorn-globals": "^6.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.1", + "decimal.js": "^10.3.1", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.0", + "parse5": "6.0.1", + "saxes": "^5.0.1", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.0.0", + "w3c-hr-time": "^1.0.2", + "w3c-xmlserializer": "^3.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^10.0.0", + "ws": "^8.2.3", + "xml-name-validator": "^4.0.0" + } + }, + "parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" + }, + "saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "requires": { + "xmlchars": "^2.2.0" + } + }, + "tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + } + }, + "tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "requires": { + "punycode": "^2.1.1" + } + }, + "universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==" + }, + "w3c-xmlserializer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz", + "integrity": "sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg==", + "requires": { + "xml-name-validator": "^4.0.0" + } + }, + "webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" + }, + "whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==" + }, + "whatwg-url": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-10.0.0.tgz", + "integrity": "sha512-CLxxCmdUby142H5FZzn4D8ikO1cmypvXVQktsgosNy4a4BHrDHeciBBGZhb0bNoR5/MltoCatso+vFjjGx8t0w==", + "requires": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + } + }, "zod": { "version": "3.25.76", "resolved": "https://mirrors.tencent.com/npm/zod/-/zod-3.25.76.tgz", @@ -24621,9 +24979,9 @@ } }, "typescript": { - "version": "5.9.3", - "resolved": "https://mirrors.tencent.com/npm/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==" + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==" }, "uc.micro": { "version": "2.1.0", @@ -24908,6 +25266,12 @@ "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==" }, + "vscode-uri": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.2.0.tgz", + "integrity": "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==", + "dev": true + }, "vue": { "version": "3.5.32", "resolved": "https://mirrors.tencent.com/npm/vue/-/vue-3.5.32.tgz", @@ -24985,6 +25349,16 @@ "@vue/devtools-api": "^6.6.4" } }, + "vue-tsc": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.4.tgz", + "integrity": "sha512-3EVHlxtpMXcb5bCaK7QDFTbEkMusDfVk0HVRrkv5hEb+Clpu9a96lKUXJAeD/akRlkoA4H8MCHgBDN19S6FnzA==", + "dev": true, + "requires": { + "@volar/typescript": "~2.4.11", + "@vue/language-core": "2.2.4" + } + }, "w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", diff --git a/manager/frontend/package.json b/manager/frontend/package.json index 62414838dacad738049d4d1377dbc319baeb5482..9d555504b2478ea37f33de5efe6c06eacdc85185 100755 --- a/manager/frontend/package.json +++ b/manager/frontend/package.json @@ -3,7 +3,7 @@ "version": "0.6.2", "type": "module", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "scripts": { "dev:mock": "vite --open --mode mock", @@ -16,10 +16,14 @@ "preview": "vite preview", "lint": "ESLINT_USE_FLAT_CONFIG=false eslint --ext .vue,.js,.jsx,.ts,.tsx ./ --max-warnings 0", "lint:fix": "ESLINT_USE_FLAT_CONFIG=false eslint --ext .vue,.js,.jsx,.ts,.tsx ./ --max-warnings 0 --fix", + "typecheck": "vue-tsc --noEmit -p tsconfig.json", + "typecheck:repository": "node scripts/typecheck-repository.mjs", "stylelint": "stylelint src/**/*.{html,vue,sass,less}", "stylelint:fix": "stylelint --cache --fix src/**/*.{html,vue,vss,sass,less}", "prepare": "node -e \"if(require('fs').existsSync('.git')){process.exit(1)}\" || is-ci || husky install", "test": "echo \"no test specified,work in process\"", + "test:repository": "vitest run --config vitest.repository.config.mts", + "test:operations": "vitest run --config vitest.operations.config.mts", "test:coverage": "echo \"no test:coverage specified,work in process\"" }, "dependencies": { @@ -54,7 +58,7 @@ "tdesign-vue-next": "1.10.7", "true-myth": "^9.3.1", "tvision-color": "~1.6.0", - "typescript": "^5.7.3", + "typescript": "5.7.3", "uuid": "^11.1.1", "vditor": "^3.10.9", "vite-svg-loader": "^5.1.0", @@ -83,17 +87,21 @@ "less": "^4.1.0", "lint-staged": "^15.4.3", "mockjs": "^1.1.0", + "postcss-html": "1.5.0", + "postcss-less": "6.0.0", "prettier": "^3.5.2", "stylelint": "~16.14.1", - "stylelint-config-prettier": "~9.0.3", - "stylelint-less": "3.0.1", "stylelint-order": "~6.0.4", "terser": "^5.18.2", "vite": "^6.2.4", "vite-plugin-mock": "^3.0.2", "vite-plugin-require-transform": "^1.0.12", "vite-plugin-theme": "^0.8.1", - "vitest": "^4.1.9" + "vitest": "^4.1.9", + "vue-tsc": "2.2.4" + }, + "overrides": { + "cherry-markdown": "0.11.0" }, "config": { "commitizen": { diff --git a/manager/frontend/scripts/typecheck-repository.mjs b/manager/frontend/scripts/typecheck-repository.mjs new file mode 100644 index 0000000000000000000000000000000000000000..05a4b47988939c3a3684c13b06bcfd31d04755f6 --- /dev/null +++ b/manager/frontend/scripts/typecheck-repository.mjs @@ -0,0 +1,32 @@ +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const vueTsc = path.join(root, 'node_modules', 'vue-tsc', 'bin', 'vue-tsc.js'); +const result = spawnSync(process.execPath, [vueTsc, '--noEmit', '-p', 'tsconfig.repository.json'], { + cwd: root, + encoding: 'utf8', + maxBuffer: 32 * 1024 * 1024, +}); + +const output = `${result.stdout || ''}${result.stderr || ''}`; +process.stdout.write(output); + +const repositoryDiagnostic = + /^(?:src\/pages\/repository\/[^\r\n(]+|src\/service\/repository\.ts|src\/pages\/ocai\/pages\/Chat\.vue)\(/; +const relevant = output.split(/\r?\n/).filter((line) => repositoryDiagnostic.test(line)); + +if (relevant.length > 0) { + console.error(`Repository vue-tsc: FAIL (${relevant.length} relevant diagnostics)`); + process.exit(1); +} +if (result.error) { + console.error(`Repository vue-tsc could not start: ${result.error.message}`); + process.exit(2); +} +if (result.status !== 0) { + console.warn('Repository vue-tsc: PASS; remaining diagnostics are outside the Repository/OCAI integration scope.'); +} else { + console.log('Repository vue-tsc: PASS'); +} diff --git a/manager/frontend/src/layouts/index.vue b/manager/frontend/src/layouts/index.vue index 1d6be78cf8232a68bdeb5008829144c3d311963b..cc31732ee26a7a3c182ae55a7f66c25f8b189fc9 100755 --- a/manager/frontend/src/layouts/index.vue +++ b/manager/frontend/src/layouts/index.vue @@ -1,5 +1,5 @@ - 登录 + + {{ submitting ? '正在登录' : '登录' }} + diff --git a/manager/frontend/src/pages/monitor/commandHelper/drawerContent.vue b/manager/frontend/src/pages/monitor/commandHelper/drawerContent.vue index d0a6b14839dc1e96b0be5acab27b4c0becd41027..e3914b9f054a0e53d1b01e5a03c7d9bde14594c1 100755 --- a/manager/frontend/src/pages/monitor/commandHelper/drawerContent.vue +++ b/manager/frontend/src/pages/monitor/commandHelper/drawerContent.vue @@ -15,7 +15,7 @@ {{ data.timeout }} min - + +
@@ -72,6 +73,7 @@ border :data="tableData" :columns="columns" + :loading="tableLoading || restoringTargets" rowKey="ip" v-model:selected-row-keys="selectedRowKeysPlain" :pagination="pagination" @@ -104,6 +106,9 @@ import { SearchIcon } from 'tdesign-icons-vue-next'; import debounce from 'lodash/debounce'; import CodeEditor from '@/components/code-editor/index.vue'; import { GLOBAL_TERMS } from '@/config/global'; +import { + commandErrorMessage, commandParameters, commandTargetIPs, loadCommandRerunTargets, selectedCommandInstances, +} from './rerun'; export default { name: 'DrawerContent', @@ -167,30 +172,75 @@ export default { }, selectedRowKeys: [], selectedRowKeysPlain: [], + tableLoading: false, + restoringTargets: false, + targetWarning: '', + contentGeneration: 0, + queryGeneration: 0, + contentClosed: false, }; }, + watch: { + data() { + this.initializeContent(); + }, + }, mounted() { - this.paramsList = this.data.params; - this.paramsList.forEach((item) => { - if (item.type === 'if') { - item.value = Boolean(item.default === 'true'); - } else { - item.value = item.default; - } - }); - this.searchData.ip = this.initIp ?? null; - this.$nextTick(() => { - this.remoteMethods(); - this.onSearch(); - }); + this.initializeContent(); + }, + beforeUnmount() { + this.contentGeneration += 1; + this.queryGeneration += 1; + this.contentClosed = true; + this.remoteMethods.cancel?.(); }, methods: { + initializeContent() { + this.contentGeneration += 1; + this.queryGeneration += 1; + this.paramsList = commandParameters(this.data?.params || []); + this.searchData.ip = this.initIp ?? null; + this.pagination.current = 1; + this.selectedRowKeys = []; + this.selectedRowKeysPlain = []; + this.targetWarning = ''; + this.restoringTargets = false; + if (!this.data?.is_run) return; + this.remoteMethods(); + this.queryData(); + this.restoreRerunTargets(); + }, + async restoreRerunTargets() { + const ips = commandTargetIPs(this.data?.rerunIp); + if (!ips.length || this.initIp) return; + const generation = this.contentGeneration; + this.restoringTargets = true; + try { + const { pageSize } = this.pagination; + const result = await loadCommandRerunTargets(ips, (page) => projectsApi.getInstanceList({ + project_id: Number(this.$route.query.project_id), status: 2, ip_list: ips, + page_request: { page_size: pageSize, page_number: page }, + }), pageSize); + if (generation !== this.contentGeneration) return; + this.selectedRowKeys = result.instances; + this.selectedRowKeysPlain = result.instances.map((instance) => instance.ip); + if (result.missing.length) this.targetWarning = `原任务目标当前不可用,请重新选择:${result.missing.join('、')}`; + } catch (error) { + if (generation === this.contentGeneration) { + this.targetWarning = commandErrorMessage(error, '原任务目标加载失败,请重新选择'); + } + } finally { + if (generation === this.contentGeneration) this.restoringTargets = false; + } + }, formatDate(value) { return value ? dayjs(value).format('YYYY年MM月DD日') : ''; }, rehandleSelectChange(value, { selectedRowData }) { this.selectedRowKeysPlain = value; - this.selectedRowKeys = selectedRowData; + this.selectedRowKeys = selectedCommandInstances(value, [ + ...this.selectedRowKeys, ...this.tableData, ...(selectedRowData || []), + ]); }, rehandlePageChange(curr) { const { current, pageSize } = curr; @@ -203,6 +253,8 @@ export default { this.queryData(); }, remoteMethods: debounce(async function remoteMethods(keyword = '') { + if (this.contentClosed) return; + const generation = this.contentGeneration; try { this.ipLoading = true; const { project_id } = this.$route.query; @@ -216,18 +268,23 @@ export default { }, }; const { instances = [] } = await projectsApi.getInstanceList(params); + if (this.contentClosed || generation !== this.contentGeneration) return; this.ipOptions = instances.map((item) => ({ label: item.ip, value: item.ip, })); this.ipLoading = false; } catch (error) { + if (this.contentClosed || generation !== this.contentGeneration) return; this.ipLoading = false; this.ipOptions = []; this.$message.error(error?.msg || '获取IP列表失败'); } }, 300), async queryData() { + this.queryGeneration += 1; + const generation = this.queryGeneration; + this.tableLoading = true; try { const { current, pageSize } = this.pagination; const { ip } = this.searchData; @@ -240,7 +297,7 @@ export default { .filter((ip) => ip !== ''), } : {}), - project_id: this.$route.query.project_id, + project_id: Number(this.$route.query.project_id), status: 2, page_request: { page_size: pageSize, @@ -249,25 +306,16 @@ export default { }; const request_item = 'getInstanceList'; const { instances, page_response } = await projectsApi[request_item](params); - this.tableData = instances; - this.pagination.total = Number(page_response.count); - - if (this.data.rerunIp) { - const ipsToSelect = typeof this.data.rerunIp === 'string' - ? this.data.rerunIp.split(/[,\s;\|]+/).map(ip => ip.trim()) - : (Array.isArray(this.data.rerunIp) ? this.data.rerunIp : []); - const matchedInstances = this.tableData.filter(item => ipsToSelect.includes(item.ip)); - if (matchedInstances.length > 0) { - this.selectedRowKeys = matchedInstances; - this.selectedRowKeysPlain = matchedInstances.map(item => item.ip); - } else { - this.$message.warning(`原任务目标IP 【${ipsToSelect.join(', ')}】 在当前项目中未找到或已失效,请重新选择!`); - } - } + if (generation !== this.queryGeneration) return; + this.tableData = instances || []; + this.pagination.total = Number(page_response?.count || 0); } catch (error) { + if (generation !== this.queryGeneration) return; this.tableData = []; this.pagination.total = 0; - this.$message.error(error?.msg || '获取实例列表失败'); + this.$message.error(commandErrorMessage(error, '获取实例列表失败')); + } finally { + if (generation === this.queryGeneration) this.tableLoading = false; } }, rules(param) { diff --git a/manager/frontend/src/pages/monitor/commandHelper/index.vue b/manager/frontend/src/pages/monitor/commandHelper/index.vue index 07d565ebcd8b916e18dab2d097e9a098edfe9d3b..dd366e81062f6f8bd0945ca0f873a3cc172607bb 100755 --- a/manager/frontend/src/pages/monitor/commandHelper/index.vue +++ b/manager/frontend/src/pages/monitor/commandHelper/index.vue @@ -124,17 +124,17 @@ - @@ -165,6 +165,9 @@ import SearchBox from '@/components/table-list/components/search-box.vue'; import CreateForm from './createForm.vue'; import { getUserStore } from '@/store/modules/user'; import { GLOBAL_TERMS } from '@/config/global'; +import { + commandErrorMessage, missingCommandParameters, prepareCommandRerun, serializeCommandParameters, +} from './rerun'; export default { name: 'CommandHelper', @@ -184,6 +187,9 @@ export default { ), dataLoading: false, + previewLoading: false, + submitLoading: false, + rerunLoading: false, curTab: '1', typesOptions: [], domainOptions: [], @@ -453,6 +459,10 @@ export default { } }, handlerClose () { + if (this.submitLoading) { + this.$message.warning('命令正在下发,请等待结果'); + return; + } const { path, query } = this.$route; const { id, ...newQuery } = query; // 获取除id外的参数作为新query,快速入口有ip和tab参数,需要去除 // 只有当 id 存在时才需要更新路由,避免不必要的导航 @@ -471,115 +481,98 @@ export default { this.$message.error('复制失败'); }); }, - async handlerPreview () { - const { - drawerContent: { paramsList }, - } = this.$refs; - const index = paramsList.findIndex((v) => v.need && !v.value); - const requiredParams = paramsList.filter((v) => v.need && !v.value)?.map((p) => p.name); - if (index < 0) { - try { - const { project_id, id } = this.$route.query; - const params = { - cmd_id: Number(id), - project_id: Number(project_id), - params: paramsList.map((v) => ({ - name: v.name, - value: v.value.toString(), - })), - }; - const { cmd } = await monitorApi.buildActionCmd(params); - this.confirmData = { type: 'preview', cmd, paramsList }; - this.isShowFormModal = true; - } catch (error) { - this.$message.error(error.msg); - } - } else { - this.$message.error(`【${requiredParams?.join('、')}】不能为空`); - } + commandForm (requireTargets = false) { + const form = this.$refs.drawerContent; + if (!form || form.restoringTargets || form.tableLoading) throw new Error('正在准备命令和目标主机,请稍后'); + const id = this.detailData?.id; + if (!this.detailData?.is_run || !/^[1-9]\d*$/.test(String(id))) throw new Error('当前命令不支持执行,请重新打开详情'); + const required = missingCommandParameters(form.paramsList); + if (required.length) throw new Error(`【${required.join('、')}】不能为空`); + const selected = this.initIp ? [{ ip: this.initIp }] : form.selectedRowKeys; + if (requireTargets && !selected.length) throw new Error(`请选择${GLOBAL_TERMS.INSTANCE}`); + return { + request: { + cmd_id: String(id), project_id: Number(this.$route.query.project_id), + params: serializeCommandParameters(form.paramsList), + }, + selected, paramsList: form.paramsList.map((param) => ({ ...param })), + }; }, - async handlerConfirm () { - const { - drawerContent: { paramsList, selectedRowKeys }, - } = this.$refs; - if (!selectedRowKeys.length && !this.initIp) return this.$message.error(`请选择${GLOBAL_TERMS.INSTANCE}!`); - const index = paramsList.findIndex((v) => v.need && !v.value); - const requiredParams = paramsList.filter((v) => v.need && !v.value)?.map((p) => p.name); - - if (index < 0) { - try { - const { project_id, id } = this.$route.query; - const ips = selectedRowKeys.map((v) => v.ip); - const projects = selectedRowKeys.map((v) => v.projects); - const selectedIp = this.initIp ? [this.initIp] : ips; - - const params = { - cmd_id: Number(id), - project_id: Number(project_id), - params: paramsList.map((v) => ({ - name: v.name, - value: v.value.toString(), - })), - }; - const { cmd } = await monitorApi.buildActionCmd(params); - if (cmd) { - this.confirmData = { cmd, selectedRowKeys, paramsList }; - this.isShowFormModal = true; - } else { - this.isShowFormModal = false; - this.confirmData = null; - this.$message.error('执行失败'); - } - } catch (error) { - console.log(error); - this.$message.error(error.msg); - } - } else { - this.$message.error(`【${requiredParams?.join('、')}】不能为空`); + async prepareCommand (preview) { + if (this.previewLoading || this.submitLoading) return; + this.previewLoading = true; + try { + const { request, selected, paramsList } = this.commandForm(!preview); + const { cmd } = await monitorApi.buildActionCmd(request); + if (!this.visible || String(this.detailData?.id) !== request.cmd_id) { + throw new Error('命令详情已关闭或变化,请重新打开'); + } + if (!cmd) throw new Error('未能生成执行命令,请检查参数后重试'); + this.confirmData = { + type: preview ? 'preview' : 'execute', cmd, paramsList, selectedRowKeys: selected, + request: { ...request, ip: selected.map((instance) => instance.ip) }, + }; + this.isShowFormModal = true; + } catch (error) { + this.$message.error(commandErrorMessage(error, '生成命令失败,请稍后重试')); + } finally { + this.previewLoading = false; } }, + handlerPreview () { + return this.prepareCommand(true); + }, + handlerConfirm () { + return this.prepareCommand(false); + }, onClose () { + if (this.submitLoading) { + this.$message.warning('命令正在下发,请等待结果'); + return; + } this.isShowFormModal = false; this.confirmData = null; }, async onConfirm () { - const { - drawerContent: { paramsList, selectedRowKeys }, - } = this.$refs; + if (this.submitLoading) return; + if (this.confirmData?.type !== 'execute' || !this.confirmData.request) { + this.$message.error('请先预览并确认执行命令'); + return; + } + this.submitLoading = true; try { - const { project_id, id } = this.$route.query; - const ips = selectedRowKeys.map((v) => v.ip); - const params = { - cmd_id: Number(id), - project_id: Number(project_id), - ip: this.initIp ? [this.initIp] : ips, - params: paramsList.map((v) => ({ - name: v.name, - value: v.value.toString(), - })), - }; + const params = this.confirmData.request; + const ips = params.ip; const { task_id } = await monitorApi.DoActions(params); + this.submitLoading = false; + let taskIDs = []; + if (Array.isArray(task_id)) taskIDs = task_id; + else if (task_id) taskIDs = [task_id]; + if (!taskIDs.length) throw new Error('下发响应未返回任务标识,请先查询执行记录,勿重复下发'); this.onClose(); this.handlerClose(); - if (task_id?.length == 1) { + if (taskIDs.length === 1) { // 单个ip跳转任务详情 if (this.$route.path === '/monitor/commandHelper') { this.$router.push( - `/monitor/commandHelper/log?task_id=${task_id || 0}&ip=${this.initIp ?? ips.toString()}&project_id=${project_id}`, + `/monitor/commandHelper/log?task_id=${taskIDs[0]}&ip=${ips.toString()}&project_id=${params.project_id}`, ); } else { - this.$router.push(`/ipDetailLog?task_id=${task_id || 0}&ip=${this.initIp ?? ip}`); + this.$router.push(`/ipDetailLog?task_id=${taskIDs[0]}&ip=${ips.toString()}`); } } else { // 多个ip跳转任务记录 this.curTab = '2'; } } catch (error) { - console.log(error); - this.$message.error(error.msg); + this.$message.error(commandErrorMessage(error, '命令下发失败,请检查执行记录后重试')); + } finally { + this.submitLoading = false; } }, async handleRerun (row) { + if (this.rerunLoading || this.submitLoading || this.previewLoading) return; + this.rerunLoading = true; try { this.dataLoading = true; const actionRes = await monitorApi.fetchDescribeAction({ task_id: String(row.task_id) }); @@ -598,34 +591,15 @@ export default { throw new Error('获取命令详情失败'); } - let parsedParams = []; - if (actionRes.params) { - try { - parsedParams = typeof actionRes.params === 'string' ? JSON.parse(actionRes.params) : actionRes.params; - } catch (e) { - console.error('解析任务参数失败', e); - } - } - if (cmdRes.params && cmdRes.params.length > 0) { - cmdRes.params.forEach((param) => { - const originalParam = parsedParams.find(p => p.name === param.name); - if (originalParam) { - param.default = originalParam.value; - } - }); - } - - this.detailData = cmdRes; - this.detailData.rerunIp = actionRes.ip || row.ip; - - this.curTab = '1'; - setTimeout(() => { - this.visible = true; - this.dataLoading = false; - }, 300); + this.detailData = prepareCommandRerun(cmdRes, actionRes, row.ip); + this.curTab = cmdRes.user === 'global' ? '1' : '3'; + await this.$nextTick(); + this.visible = true; } catch (error) { + this.$message.error(commandErrorMessage(error, '重跑任务失败')); + } finally { this.dataLoading = false; - this.$message.error(error.message || '重跑任务失败'); + this.rerunLoading = false; } }, }, diff --git a/manager/frontend/src/pages/monitor/commandHelper/rerun-drawer.test.ts b/manager/frontend/src/pages/monitor/commandHelper/rerun-drawer.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..dd707797425ac7165e07270661c1ac80cab616d9 --- /dev/null +++ b/manager/frontend/src/pages/monitor/commandHelper/rerun-drawer.test.ts @@ -0,0 +1,70 @@ +import { flushPromises, mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import { Drawer } from 'tdesign-vue-next'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import CommandHelper from './index.vue'; + +vi.mock('@/service/index', () => ({ + monitorApi: { + fetchListCmdTypes: vi.fn(async () => ({ types: [], domains: [] })), + fetchListGlobalCmds: vi.fn(async () => ({ cmds: [], page_response: { count: 0 } })), + fetchDescribeAction: vi.fn(async () => ({ + cmd_id: '7', ip: ['192.0.2.10'], params: [{ name: 'path', value: '/etc/os-release' }], + })), + fetchDescribeCmd: vi.fn(async () => ({ + id: '7', title: 'Read file', is_run: true, user: 'global', params: [{ name: 'path', type: 'string' }], + })), + DoActions: vi.fn(), + }, +})); +vi.mock('@/store/modules/user', () => ({ getUserStore: () => ({ userInfo: { login_name: 'test' } }) })); +vi.mock('./drawerContent.vue', () => ({ + default: { + props: ['data'], + template: '
{{ data.params[0].default }} {{ data.rerunIp[0] }}
', + }, +})); +vi.mock('./logsList.vue', () => ({ default: { template: '
History
' } })); +vi.mock('./confirmForm.vue', () => ({ default: { template: '
' } })); +vi.mock('./createForm.vue', () => ({ default: { template: '
' } })); +vi.mock('@/components/table-list/components/search-box.vue', () => ({ default: { template: '
' } })); + +afterEach(() => { + vi.useRealTimers(); + document.body.innerHTML = ''; +}); + +describe('command history drawer with the installed TDesign', () => { + it('keeps an immediately opened rerun form mounted after switching away from history', async () => { + vi.useFakeTimers(); + const wrapper = mount(CommandHelper, { + attachTo: document.body, + global: { + components: { TDrawer: Drawer }, + stubs: { + TButton: { template: '' }, + TTabs: true, TTabPanel: true, TForm: true, TFormItem: true, + TInputAdornment: true, TSelect: true, TInput: true, TPagination: true, + TRadioButton: true, TRadioGroup: true, TCard: true, + }, + directives: { loading: () => {} }, + mocks: { + $route: { path: '/monitor/commandHelper', query: { project_id: '1' } }, + $message: { error: vi.fn() }, + }, + }, + }); + await flushPromises(); + await wrapper.setData({ curTab: '2' }); + await wrapper.vm.handleRerun({ task_id: 'history' }); + await nextTick(); + await vi.advanceTimersByTimeAsync(500); + await flushPromises(); + expect(wrapper.vm.visible).toBe(true); + expect(document.querySelector('[data-test="rerun-form"]')?.textContent).toContain('/etc/os-release'); + expect(document.querySelector('[data-test="rerun-form"]')?.textContent).toContain('192.0.2.10'); + const { monitorApi } = await import('@/service/index'); + expect(monitorApi.DoActions).not.toHaveBeenCalled(); + wrapper.unmount(); + }); +}); diff --git a/manager/frontend/src/pages/monitor/commandHelper/rerun.test.ts b/manager/frontend/src/pages/monitor/commandHelper/rerun.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..c6c321a185ff0c65044ee558028dfe14be4b027c --- /dev/null +++ b/manager/frontend/src/pages/monitor/commandHelper/rerun.test.ts @@ -0,0 +1,299 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import CommandHelper from './index.vue'; +import DrawerContent from './drawerContent.vue'; +import { + commandErrorMessage, + commandParameters, + commandTargetIPs, + loadCommandRerunTargets, + missingCommandParameters, + prepareCommandRerun, + selectedCommandInstances, + serializeCommandParameters, +} from './rerun'; + +const { monitorApi, projectsApi } = vi.hoisted(() => ({ + monitorApi: { + fetchDescribeAction: vi.fn(), + fetchDescribeCmd: vi.fn(), + buildActionCmd: vi.fn(), + DoActions: vi.fn(), + }, + projectsApi: { getInstanceList: vi.fn() }, +})); +vi.mock('@/service/index', () => ({ monitorApi, projectsApi })); +vi.mock('@/store/modules/user', () => ({ getUserStore: () => ({ userInfo: {} }) })); +vi.mock('@/components/code-editor/index.vue', () => ({ default: { template: '
' } })); +vi.mock('@/components/table-list/components/search-box.vue', () => ({ default: { template: '
' } })); +vi.mock('./logsList.vue', () => ({ default: { template: '
' } })); +vi.mock('./confirmForm.vue', () => ({ default: { template: '
' } })); +vi.mock('./createForm.vue', () => ({ default: { template: '
' } })); + +beforeEach(() => vi.clearAllMocks()); + +describe('command rerun data', () => { + it('clones historical parameters without losing zero, false or empty values', () => { + const source = { + id: '25', + is_run: true, + params: [ + { name: 'count', default: 7 }, + { name: 'enabled', type: 'if', default: true }, + { name: 'text', default: 'old' }, + ], + }; + const result = prepareCommandRerun(source, { + params: JSON.stringify([ + { name: 'count', value: 0 }, + { name: 'enabled', value: false }, + { name: 'text', value: '' }, + ]), + ip: '192.0.2.1, 192.0.2.2', + }); + const parameters = commandParameters(result.params); + expect(parameters.map((parameter) => parameter.value)).toEqual([0, false, '']); + expect(source.params[0].default).toBe(7); + expect(result.rerunIp).toEqual(['192.0.2.1', '192.0.2.2']); + expect(missingCommandParameters(parameters.map((parameter) => ({ ...parameter, need: true })))).toEqual(['text']); + expect(serializeCommandParameters(parameters).map((parameter) => parameter.value)).toEqual(['0', 'false', '']); + }); + + it('keeps custom shell content editable and handles a command without parameters', () => { + expect(commandParameters([{ name: 'sh', value: 'printf ok' }])[0]).toMatchObject({ + type: 'textarea', + value: 'printf ok', + }); + expect(commandParameters()).toEqual([]); + expect(commandParameters([{ name: 'x', default: null, value: 'stale' }])[0].value).toBe(''); + }); + + it('rejects invalid history and respects the backend execution capability', () => { + expect(() => prepareCommandRerun({ id: 25, is_run: true }, { params: 'invalid' })).toThrow('历史任务参数格式无效'); + expect(() => prepareCommandRerun({ id: 25, is_run: true }, { params: '{}' })).toThrow('历史任务参数格式无效'); + expect(() => prepareCommandRerun({ id: 25, is_run: false }, {})).toThrow('不支持下发执行'); + expect(() => prepareCommandRerun({ id: 25, is_run: true }, { params: [{ value: 'x' }] })).toThrow('缺少名称'); + const empty = prepareCommandRerun( + { id: 25, is_run: true, params: [{ name: 'x', default: 'old' }] }, + { params: [{ name: 'x' }] }, + ); + expect(commandParameters(empty.params)[0].value).toBe(''); + }); + + it('normalizes target addresses and uses readable API/local error messages', () => { + expect(commandTargetIPs('192.0.2.1;192.0.2.2 | 192.0.2.1')).toEqual(['192.0.2.1', '192.0.2.2']); + expect(commandErrorMessage({ msg: '接口失败' }, '兜底')).toBe('接口失败'); + expect(commandErrorMessage(new Error('表单失败'), '兜底')).toBe('表单失败'); + expect(commandErrorMessage({}, '兜底')).toBe('兜底'); + }); + + it('loads explicitly requested rerun targets across pages rather than the visible table page', async () => { + const ips = ['192.0.2.21', '192.0.2.22', '192.0.2.23']; + const loadPage = vi + .fn() + .mockResolvedValueOnce({ instances: ips.slice(0, 2).map((ip) => ({ ip })), page_response: { count: '3' } }) + .mockResolvedValueOnce({ instances: [{ ip: ips[2] }], page_response: { count: '3' } }); + const result = await loadCommandRerunTargets(ips, loadPage, 2); + expect(loadPage.mock.calls).toEqual([[1], [2]]); + expect(result.instances.map((instance) => instance.ip)).toEqual(ips); + expect(result.missing).toEqual([]); + }); + + it('reports absent targets and stops a repeated page without looping forever', async () => { + const loadPage = vi.fn().mockResolvedValue({ instances: [{ ip: '192.0.2.1' }], page_response: { count: '50' } }); + const result = await loadCommandRerunTargets(['192.0.2.1', '192.0.2.2'], loadPage, 1); + expect(loadPage).toHaveBeenCalledTimes(2); + expect(result.missing).toEqual(['192.0.2.2']); + }); + + it('continues past a page without matching targets', async () => { + const loadPage = vi + .fn() + .mockResolvedValueOnce({ instances: [{ ip: '192.0.2.1' }], page_response: { count: '2' } }) + .mockResolvedValueOnce({ instances: [{ ip: '192.0.2.21' }], page_response: { count: '2' } }); + const result = await loadCommandRerunTargets(['192.0.2.21'], loadPage, 1); + expect(loadPage.mock.calls).toEqual([[1], [2]]); + expect(result.missing).toEqual([]); + }); + + it('retains off-page selected rows but honors explicit deselection', () => { + expect( + selectedCommandInstances(['192.0.2.21'], [{ ip: '192.0.2.21', asset_id: 'old' }, { ip: '192.0.2.1' }]), + ).toEqual([{ ip: '192.0.2.21', asset_id: 'old' }]); + expect(selectedCommandInstances([], [{ ip: '192.0.2.21' }])).toEqual([]); + }); + + it('continues past empty pages when the reported total includes later targets', async () => { + const loadPage = vi.fn() + .mockResolvedValueOnce({ instances: [], page_response: { count: '3' } }) + .mockResolvedValueOnce({ instances: [], page_response: { count: '3' } }) + .mockResolvedValueOnce({ instances: [{ ip: '192.0.2.21' }], page_response: { count: '3' } }); + const result = await loadCommandRerunTargets(['192.0.2.21'], loadPage, 1); + expect(loadPage.mock.calls).toEqual([[1], [2], [3]]); + expect(result.missing).toEqual([]); + }); + + it('bounds empty-page recovery to the first total even when totals keep increasing', async () => { + const loadPage = vi.fn() + .mockResolvedValueOnce({ instances: [], page_response: { count: '2' } }) + .mockResolvedValue({ instances: [], page_response: { count: '100' } }); + const result = await loadCommandRerunTargets(['192.0.2.21'], loadPage, 1); + expect(loadPage.mock.calls).toEqual([[1], [2]]); + expect(result.missing).toEqual(['192.0.2.21']); + }); + + it.each([0, -1, NaN, Infinity, 1.5])('rejects invalid page size %s before requesting targets', async (pageSize) => { + const loadPage = vi.fn(); + await expect(loadCommandRerunTargets(['192.0.2.21'], loadPage, pageSize)).rejects.toThrow('分页大小无效'); + expect(loadPage).not.toHaveBeenCalled(); + }); + + it('does not request pages without targets and stops with a missing or invalid total', async () => { + const loadPage = vi.fn().mockResolvedValue({ instances: [], page_response: { count: 'invalid' } }); + expect(await loadCommandRerunTargets([], loadPage, 20)).toEqual({ instances: [], missing: [] }); + expect(loadPage).not.toHaveBeenCalled(); + expect((await loadCommandRerunTargets(['192.0.2.21'], loadPage, 20)).missing).toEqual(['192.0.2.21']); + expect(loadPage).toHaveBeenCalledTimes(1); + }); +}); + +function commandContext() { + const { methods } = CommandHelper as any; + const context: any = { + detailData: { id: '25', is_run: true }, + initIp: null, + previewLoading: false, + submitLoading: false, + rerunLoading: false, + visible: true, + $route: { query: { project_id: '3' }, path: '/monitor/commandHelper' }, + $refs: { + drawerContent: { paramsList: [{ name: 'count', value: 0, need: true }], selectedRowKeys: [{ ip: '192.0.2.21' }] }, + }, + $message: { error: vi.fn(), warning: vi.fn() }, + $router: { push: vi.fn() }, + $nextTick: vi.fn().mockResolvedValue(undefined), + onClose: vi.fn(), + handlerClose: vi.fn(), + }; + context.commandForm = methods.commandForm.bind(context); + context.prepareCommand = methods.prepareCommand.bind(context); + return context; +} + +describe('command helper method contract', () => { + it('uses the loaded command ID after rerun even when the URL has no id', async () => { + const context = commandContext(); + monitorApi.buildActionCmd.mockResolvedValueOnce({ cmd: 'printf ok' }); + await (CommandHelper as any).methods.handlerConfirm.call(context); + expect(monitorApi.buildActionCmd).toHaveBeenCalledWith({ + cmd_id: '25', + project_id: 3, + params: [{ name: 'count', value: '0' }], + }); + expect(monitorApi.DoActions).not.toHaveBeenCalled(); + expect(context.confirmData.request.ip).toEqual(['192.0.2.21']); + }); + + it('does not execute when opening a historical task', async () => { + const context = commandContext(); + monitorApi.fetchDescribeAction.mockResolvedValueOnce({ + cmd_id: '25', + params: '[{"name":"count","value":0}]', + ip: '192.0.2.21', + }); + monitorApi.fetchDescribeCmd.mockResolvedValueOnce({ + id: '25', + is_run: true, + user: 'admin', + params: [{ name: 'count', default: '2' }], + }); + await (CommandHelper as any).methods.handleRerun.call(context, { task_id: 'history' }); + expect(context.detailData.params[0].default).toBe(0); + expect(context.curTab).toBe('3'); + expect(context.visible).toBe(true); + expect(context.rerunLoading).toBe(false); + expect(monitorApi.buildActionCmd).not.toHaveBeenCalled(); + expect(monitorApi.DoActions).not.toHaveBeenCalled(); + }); + + it('reports validation and local exceptions instead of failing silently', async () => { + const context = commandContext(); + context.$refs.drawerContent.selectedRowKeys = []; + await (CommandHelper as any).methods.handlerConfirm.call(context); + expect(context.$message.error).toHaveBeenCalled(); + expect(monitorApi.buildActionCmd).not.toHaveBeenCalled(); + context.$refs.drawerContent.selectedRowKeys = [{ ip: '192.0.2.21' }]; + monitorApi.buildActionCmd.mockRejectedValueOnce(new Error('网络断开')); + await (CommandHelper as any).methods.handlerConfirm.call(context); + expect(context.$message.error).toHaveBeenLastCalledWith('网络断开'); + expect(context.previewLoading).toBe(false); + }); + + it('blocks repeated preview clicks while a request is pending', async () => { + const context = commandContext(); + let finish!: (value: { cmd: string }) => void; + monitorApi.buildActionCmd.mockReturnValueOnce( + new Promise((resolve) => { + finish = resolve; + }), + ); + const first = (CommandHelper as any).methods.handlerPreview.call(context); + await (CommandHelper as any).methods.handlerPreview.call(context); + expect(monitorApi.buildActionCmd).toHaveBeenCalledTimes(1); + finish({ cmd: 'printf ok' }); + await first; + }); + + it('submits exactly the confirmed snapshot once and restores pending feedback', async () => { + const context = commandContext(); + context.confirmData = { + type: 'execute', + request: { cmd_id: '25', project_id: 3, ip: ['192.0.2.21'], params: [{ name: 'count', value: '0' }] }, + }; + let finish!: (value: { task_id: string[] }) => void; + monitorApi.DoActions.mockReturnValueOnce( + new Promise((resolve) => { + finish = resolve; + }), + ); + const first = (CommandHelper as any).methods.onConfirm.call(context); + await (CommandHelper as any).methods.onConfirm.call(context); + expect(monitorApi.DoActions).toHaveBeenCalledTimes(1); + finish({ task_id: ['100'] }); + await first; + expect(context.submitLoading).toBe(false); + expect(context.$router.push).toHaveBeenCalledWith( + '/monitor/commandHelper/log?task_id=100&ip=192.0.2.21&project_id=3', + ); + }); + + it('shows an API failure and allows a later explicitly confirmed attempt', async () => { + const context = commandContext(); + context.confirmData = { type: 'execute', request: { cmd_id: '25', project_id: 3, ip: ['192.0.2.21'], params: [] } }; + monitorApi.DoActions.mockRejectedValueOnce({ msg: '目标不可用' }); + await (CommandHelper as any).methods.onConfirm.call(context); + expect(context.$message.error).toHaveBeenCalledWith('目标不可用'); + expect(context.submitLoading).toBe(false); + }); + + it('does not automatically reselect deselected rerun targets when browsing another page', async () => { + const context: any = { + queryGeneration: 0, + tableLoading: false, + pagination: { current: 2, pageSize: 20 }, + searchData: { ip: null }, + data: { rerunIp: ['192.0.2.21'] }, + selectedRowKeys: [], + selectedRowKeysPlain: [], + $route: { query: { project_id: '3' } }, + $message: { error: vi.fn() }, + }; + projectsApi.getInstanceList.mockResolvedValueOnce({ + instances: [{ ip: '192.0.2.21' }], + page_response: { count: 21 }, + }); + await (DrawerContent as any).methods.queryData.call(context); + expect(context.tableData).toEqual([{ ip: '192.0.2.21' }]); + expect(context.selectedRowKeysPlain).toEqual([]); + }); +}); diff --git a/manager/frontend/src/pages/monitor/commandHelper/rerun.ts b/manager/frontend/src/pages/monitor/commandHelper/rerun.ts new file mode 100644 index 0000000000000000000000000000000000000000..8a3bea49eedc2bbfd9bcb9a1931dfecdf2b490ae --- /dev/null +++ b/manager/frontend/src/pages/monitor/commandHelper/rerun.ts @@ -0,0 +1,140 @@ +type CommandParameter = { + name: string; + type?: string; + default?: unknown; + value?: unknown; + need?: boolean; + values?: unknown[]; + [key: string]: unknown; +}; + +type CommandDetail = { + id?: string | number; + is_run?: boolean; + params?: CommandParameter[]; + [key: string]: unknown; +}; + +export type CommandInstance = { ip: string; [key: string]: unknown }; + +export function commandErrorMessage(error: unknown, fallback: string): string { + const value = error as { msg?: unknown; message?: unknown } | null; + const message = value?.msg || value?.message; + return typeof message === 'string' && message.trim() ? message : fallback; +} + +export function commandTargetIPs(value: unknown): string[] { + let values: unknown[] = []; + if (Array.isArray(value)) values = value; + else if (typeof value === 'string') values = value.split(/[,\s;|]+/); + return [ + ...new Set( + values + .filter((ip): ip is string => typeof ip === 'string') + .map((ip) => ip.trim()) + .filter(Boolean), + ), + ]; +} + +export function commandParameters(parameters: CommandParameter[] = []): CommandParameter[] { + return parameters.map((parameter) => { + const value = Object.hasOwn(parameter, 'default') ? parameter.default : (parameter.value ?? ''); + return { + ...parameter, + type: parameter.type || 'textarea', + values: parameter.values ? [...parameter.values] : [], + value: parameter.type === 'if' ? value === true || value === 'true' : (value ?? ''), + }; + }); +} + +export function prepareCommandRerun( + command: CommandDetail, + action: { params?: unknown; ip?: unknown }, + rowIP?: unknown, +) { + if (!command.id || !command.is_run) throw new Error('当前命令不存在或不支持下发执行,无法重跑'); + let parameters = action.params; + if (typeof parameters === 'string') { + try { + parameters = parameters.trim() ? JSON.parse(parameters) : []; + } catch { + throw new Error('历史任务参数格式无效,无法安全回填'); + } + } + if (parameters !== undefined && parameters !== null && !Array.isArray(parameters)) { + throw new Error('历史任务参数格式无效,无法安全回填'); + } + const previous = new Map(); + ((parameters || []) as CommandParameter[]).forEach((parameter) => { + if (!parameter || typeof parameter.name !== 'string' || !parameter.name.trim()) { + throw new Error('历史任务参数缺少名称,无法安全回填'); + } + // encoding/json omits the default empty value of protobuf CmdParam strings. + previous.set(parameter.name, Object.hasOwn(parameter, 'value') ? parameter.value : ''); + }); + return { + ...command, + params: (command.params || []).map((parameter) => + previous.has(parameter.name) ? { ...parameter, default: previous.get(parameter.name) } : { ...parameter }, + ), + rerunIp: commandTargetIPs(action.ip || rowIP), + }; +} + +export function missingCommandParameters(parameters: CommandParameter[]): string[] { + return parameters + .filter( + (parameter) => + parameter.need && + (parameter.value === null || + parameter.value === undefined || + (typeof parameter.value === 'string' && parameter.value.trim() === '')), + ) + .map((parameter) => parameter.name); +} + +export function serializeCommandParameters(parameters: CommandParameter[]) { + return parameters.map((parameter) => ({ name: parameter.name, value: String(parameter.value ?? '') })); +} + +export function selectedCommandInstances(keys: string[], known: CommandInstance[]): CommandInstance[] { + const byIP = new Map(known.filter(Boolean).map((instance) => [instance.ip, instance])); + return keys.map((key) => byIP.get(key)).filter((instance): instance is CommandInstance => Boolean(instance)); +} + +export async function loadCommandRerunTargets( + ips: string[], + loadPage: (page: number) => Promise<{ instances?: CommandInstance[]; page_response?: { count?: string | number } }>, + pageSize: number, +) { + if (!Number.isSafeInteger(pageSize) || pageSize <= 0) throw new Error('目标分页大小无效,请重新打开详情'); + const expected = new Set(ips); + const found = new Map(); + const seenPages = new Set(); + let totalPages = 1; + async function load(page: number): Promise { + if (expected.size === found.size) return; + const response = await loadPage(page); + const rows = response.instances || []; + if (page === 1) { + const count = Number(response.page_response?.count ?? rows.length); + // Snapshot the first count: changing totals must not extend recovery forever. + if (Number.isSafeInteger(count) && count >= 0) totalPages = Math.max(1, Math.ceil(count / pageSize)); + } + const signature = JSON.stringify(rows.map((instance) => instance.ip)); + if (rows.length && seenPages.has(signature)) return; + if (rows.length) seenPages.add(signature); + rows.forEach((instance) => { + if (expected.has(instance.ip)) found.set(instance.ip, instance); + }); + if (page >= totalPages) return; + await load(page + 1); + } + await load(1); + return { + instances: ips.flatMap((ip) => (found.has(ip) ? [found.get(ip)!] : [])), + missing: ips.filter((ip) => !found.has(ip)), + }; +} diff --git a/manager/frontend/src/pages/ocai/components/ChatAssistantChunkSegments.vue b/manager/frontend/src/pages/ocai/components/ChatAssistantChunkSegments.vue index c711d6f1fdf6988f2deb2f285b030fc7b00d0991..35302dd9217dcfffd39be957599219d1b92c0e21 100644 --- a/manager/frontend/src/pages/ocai/components/ChatAssistantChunkSegments.vue +++ b/manager/frontend/src/pages/ocai/components/ChatAssistantChunkSegments.vue @@ -149,11 +149,7 @@
- + @@ -165,6 +161,7 @@ import { LoadingIcon, } from 'tdesign-icons-vue-next'; import MarkdownRenderer from '@/pages/ocai/components/MarkdownRenderer.vue'; +import TChatThinking from '@/pages/ocai/components/OcaiThinking.vue'; import SseEventCard from '@/pages/ocai/components/SseEventCard.vue'; type TimelineOrSingleChunk = @@ -207,6 +204,16 @@ function suggestionCategory(sug: Record): string { return typeof c === 'string' ? c.trim() : ''; } +function fallbackContent(value: unknown): string { + if (typeof value === 'string') return value; + if (value == null) return ''; + try { + return `\`\`\`json\n${JSON.stringify(value, null, 2)}\n\`\`\``; + } catch { + return String(value); + } +} + function suggestionPrompt(sug: Record): string { const p = sug?.prompt; if (typeof p === 'string' && p.trim()) return p.trim(); diff --git a/manager/frontend/src/pages/ocai/components/OcaiChatList.vue b/manager/frontend/src/pages/ocai/components/OcaiChatList.vue new file mode 100644 index 0000000000000000000000000000000000000000..10024ec85b190982d1e7e5b777a2051310fcc8d4 --- /dev/null +++ b/manager/frontend/src/pages/ocai/components/OcaiChatList.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/manager/frontend/src/pages/ocai/components/OcaiChatSender.vue b/manager/frontend/src/pages/ocai/components/OcaiChatSender.vue new file mode 100644 index 0000000000000000000000000000000000000000..f3749227f71095ef4ef7951a0a99491c4289f4fd --- /dev/null +++ b/manager/frontend/src/pages/ocai/components/OcaiChatSender.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/manager/frontend/src/pages/ocai/components/OcaiThinking.vue b/manager/frontend/src/pages/ocai/components/OcaiThinking.vue new file mode 100644 index 0000000000000000000000000000000000000000..6f3dc8fe0e70ec68098d1ddfbf5f358e1a277409 --- /dev/null +++ b/manager/frontend/src/pages/ocai/components/OcaiThinking.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/manager/frontend/src/pages/ocai/index.vue b/manager/frontend/src/pages/ocai/index.vue index 4ef500521f8788a77518a6dac21a80b906225f50..9a8e4d1d45e537db157bcf4fb869c87e21ed7165 100644 --- a/manager/frontend/src/pages/ocai/index.vue +++ b/manager/frontend/src/pages/ocai/index.vue @@ -5,25 +5,12 @@ diff --git a/manager/frontend/src/pages/repository/components/RepositoryDistribution.vue b/manager/frontend/src/pages/repository/components/RepositoryDistribution.vue new file mode 100644 index 0000000000000000000000000000000000000000..0c27b35e71be120cf51616df4e9300290ef5b442 --- /dev/null +++ b/manager/frontend/src/pages/repository/components/RepositoryDistribution.vue @@ -0,0 +1,522 @@ + + + + + + diff --git a/manager/frontend/src/pages/repository/components/RepositoryFilterBar.test.ts b/manager/frontend/src/pages/repository/components/RepositoryFilterBar.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..8e4cb59e57ec6ed8cf6cdcacc9bcb3bb71400250 --- /dev/null +++ b/manager/frontend/src/pages/repository/components/RepositoryFilterBar.test.ts @@ -0,0 +1,87 @@ +import { defineComponent, nextTick } from 'vue'; +import { mount } from '@vue/test-utils'; +import { Button, Form, FormItem, Input, InputAdornment } from 'tdesign-vue-next'; +import { describe, expect, it, vi } from 'vitest'; +import SearchBox from '@/components/table-list/components/search-box.vue'; +import RepositoryFilterBar from './RepositoryFilterBar.vue'; + +vi.mock('@/store/modules/setting', () => ({ + getSettingStore: () => ({ isSidebarCompact: false }), +})); + +const components = { + TButton: Button, + TForm: Form, + TFormItem: FormItem, + TInput: Input, + TInputAdornment: InputAdornment, +}; + +const Harness = defineComponent({ + components: { RepositoryFilterBar }, + data: () => ({ submits: 0, resets: 0, keyword: '' }), + template: ` + + + + + + + + 创建时间 +