Compare commits

..
Author SHA1 Message Date
panxiao81 35ada6d7eb feat: 接通 Database 凭据准备闭环
Verify / test (pull_request) Successful in 11m23s
Verify / lint (pull_request) Successful in 12m18s
Verify / database-integration (pull_request) Successful in 13m49s
2026-09-27 18:35:13 +00:00
panxiao81 371248659b feat: 固定 Database 凭据位置与确认版本
Verify / test (pull_request) Successful in 13m42s
Verify / lint (pull_request) Successful in 14m26s
Verify / database-integration (pull_request) Successful in 16m16s
2026-09-27 18:08:09 +00:00
panxiao81 6b2808ce91 Merge pull request 'feat: OpenBao Kubernetes 认证与短期会话续期' (#13) from feat/database-openbao-auth into main 2026-09-27 17:48:22 +00:00
panxiao81 356e21686f refactor: 按职责拆分 controller 启动流程
Verify / test (pull_request) Successful in 9m48s
Verify / lint (pull_request) Successful in 10m42s
Verify / database-integration (pull_request) Successful in 12m1s
2026-09-27 17:37:45 +00:00
panxiao81 c7b80890fb refactor: 将 controller 装配收拢到 bootstrap 包
Verify / lint (pull_request) Successful in 9m21s
Verify / test (pull_request) Successful in 10m8s
Verify / database-integration (pull_request) Successful in 10m3s
2026-09-27 17:06:35 +00:00
panxiao81 65c60cca45 refactor: 分离公共基础设施与 Database 适配器
Verify / lint (pull_request) Successful in 13m32s
Verify / test (pull_request) Successful in 14m5s
Verify / database-integration (pull_request) Successful in 15m1s
2026-09-25 21:55:24 +00:00
panxiao81 c20f8930f0 refactor: OpenBao 认证复用 manager Kubernetes client
Verify / test (pull_request) Successful in 4m36s
Verify / lint (pull_request) Successful in 12m25s
Verify / database-integration (pull_request) Successful in 15m49s
2026-09-25 21:38:56 +00:00
panxiao81 f0aa86f676 fix: 复用 manager kubeconfig 通过 TokenRequest 登录 OpenBao
Verify / lint (pull_request) Successful in 11m47s
Verify / database-integration (pull_request) Successful in 15m24s
Verify / test (pull_request) Successful in 16m41s
2026-09-25 21:02:06 +00:00
panxiao81 dcf9ab50df feat: 接入 OpenBao Kubernetes 认证与短期会话续期
Verify / test (pull_request) Successful in 15m55s
Verify / lint (pull_request) Successful in 17m56s
Verify / database-integration (pull_request) Successful in 19m54s
2026-09-25 19:39:56 +00:00
panxiao81 22ab72ec60 Merge pull request 'feat: OpenBao 应用凭据安全存储切片' (#12) from feat/database-provisioning into main 2026-09-25 19:26:15 +00:00
panxiao81 2a4f622f44 test: 分离 OpenBao 镜像拉取与启动超时并保留诊断
Verify / test (pull_request) Successful in 5m24s
Verify / lint (pull_request) Successful in 11m53s
Verify / database-integration (pull_request) Successful in 13m32s
2026-09-25 16:30:09 +00:00
panxiao81 f6bb9e4599 feat: 增加 OpenBao 应用凭据安全存储切片
Verify / test (pull_request) Successful in 5m43s
Verify / lint (pull_request) Successful in 14m8s
Verify / database-integration (pull_request) Failing after 13m35s
2026-09-25 15:41:53 +00:00
panxiao81 f4deb98a7f Merge pull request 'feat: Instance 原生管理观测与删除保护' (#11) from feat/database-instance-observation into main 2026-09-25 14:32:25 +00:00
panxiao81 bc227bfdb4 feat: 接入 Instance 原生管理观测与删除保护
Verify / test (pull_request) Successful in 12m41s
Verify / lint (pull_request) Successful in 14m14s
Verify / database-integration (pull_request) Successful in 16m9s
2026-09-25 11:35:04 +00:00
panxiao81 55b269ce2e Merge pull request 'feat: Database 三资源 API 与分层绑定协调' (#10) from feat/database-resource-api into main 2026-09-25 04:44:13 +00:00
panxiao81 7e9e8e828b feat: 接入 Database 三资源 API 与分层绑定协调
Verify / test (pull_request) Successful in 11m39s
Verify / lint (pull_request) Successful in 12m51s
Verify / database-integration (pull_request) Successful in 13m12s
确定单库单账号、集群级 Database、资源侧先写绑定和凭据定位合同。领域层承载纯规则,service 协調流程,Kubernetes adapter 负责资源呈现与版本保护。

验证:全量 make test、三轮 race、真实 API server 并发与重启补写、最小 RBAC/watch、lint 和文档检查通过。供应、凭据交付及删除清理尚未实现,保留 DeletionPending/finalizer 边界。
2026-09-25 04:09:43 +00:00
panxiao81 347a667c0c Merge pull request 'refactor: 分离 Database 资源与申请并撤除 registry' (#9) from feat/database-registry-inspection into main 2026-09-24 17:22:53 +00:00
panxiao81 f347ee5292 ci: 避免 main 合并后重复全量验证
Verify / lint (pull_request) Successful in 16m40s
Verify / database-integration (pull_request) Successful in 17m50s
Verify / test (pull_request) Successful in 3m25s
2026-09-24 16:58:20 +00:00
panxiao81 e2e795a889 Merge pull request 'feat: 接入 Instance 版本与扩展可用性观测' (#8) from feat/database-metadata-observation into main
Verify / test (push) Successful in 10m53s
Verify / lint (push) Successful in 12m26s
Verify / database-integration (push) Successful in 16m44s
2026-09-24 16:51:48 +00:00
panxiao81 23a2d81b50 refactor: 移除 Database registry 与 Instance 初始化依赖
Verify / test (pull_request) Successful in 11m8s
Verify / lint (pull_request) Successful in 19m34s
Verify / database-integration (pull_request) Successful in 21m51s
2026-09-24 16:32:30 +00:00
panxiao81 6db8a495fb docs: 分离 Database 资源与 Tenant 申请生命周期 2026-09-24 16:21:17 +00:00
panxiao81 9441b568da feat: 接入 Instance 版本与扩展可用性观测
Verify / test (pull_request) Successful in 6m1s
Verify / lint (pull_request) Successful in 7m19s
Verify / database-integration (pull_request) Successful in 5m11s
2026-09-24 15:14:43 +00:00
panxiao81 6acba0ca46 Merge pull request 'feat: 迁移 PostgreSQL registry 所有权存储' (#7) from feat/database-registry into main
Verify / test (push) Successful in 8m36s
Verify / lint (push) Successful in 10m32s
Verify / database-integration (push) Successful in 6m9s
2026-09-24 14:46:52 +00:00
panxiao81 77848c7dc8 Merge pull request 'feat: 接入管理 Secret 凭据与连接刷新' (#6) from feat/database-admin-credentials into main
Verify / test (push) Successful in 5m45s
Verify / database-integration (push) Successful in 11m41s
Verify / lint (push) Successful in 12m38s
2026-09-24 14:45:58 +00:00
panxiao81 e590542b0b feat: 迁移 PostgreSQL registry 所有权存储与恢复测试
Verify / test (pull_request) Successful in 9m18s
Verify / lint (pull_request) Successful in 10m19s
Verify / database-integration (pull_request) Successful in 11m32s
2026-09-21 15:59:14 +00:00
panxiao81 3d91abe380 fix: 按 runner 默认提供 Docker 的约定仅执行预检
Verify / test (pull_request) Successful in 5m45s
Verify / lint (pull_request) Successful in 7m44s
Verify / database-integration (pull_request) Successful in 11m42s
2026-09-21 15:59:12 +00:00
panxiao81 fb2c34ce46 fix: 不将 Docker 数据目录独立挂载作为启动前置条件
Verify / database-integration (pull_request) Failing after 11m16s
Verify / lint (pull_request) Successful in 15m38s
Verify / test (pull_request) Successful in 16m2s
2026-09-21 15:51:10 +00:00
panxiao81 505aeb8e50 fix: 在 Pod CI 初始化 Docker 并保留 fixture 诊断
Verify / database-integration (pull_request) Failing after 58s
Verify / test (pull_request) Successful in 11m30s
Verify / lint (pull_request) Successful in 13m59s
2026-09-21 15:48:06 +00:00
panxiao81 c110aceb0f feat: 接入管理 Secret 凭据与连接刷新
Verify / test (pull_request) Successful in 4m45s
Verify / lint (pull_request) Successful in 7m5s
Verify / database-integration (pull_request) Failing after 11m1s
2026-09-21 08:35:24 +00:00
panxiao81 341c095a97 Merge pull request 'feat: 实现 Instance Ready 领域判定与恢复规则' (#5) from feat/database-instance-readiness into main
Verify / test (push) Successful in 11m36s
Verify / lint (push) Successful in 12m3s
Reviewed-on: #5
2026-09-21 07:58:31 +00:00
panxiao81 cd0d3a70ae feat: 实现 Instance Ready 领域判定与恢复规则
Verify / test (pull_request) Successful in 7m20s
Verify / lint (pull_request) Successful in 7m51s
2026-09-21 07:49:41 +00:00
panxiao81 984c0aee73 Merge pull request 'feat: 迁移 Database Instance 领域基线与扩展观测' (#4) from feat/database-instance-domain into main
Verify / test (push) Successful in 6m0s
Verify / lint (push) Successful in 6m23s
Reviewed-on: #4
2026-09-21 07:42:08 +00:00
panxiao81 813b4341d1 test: 复用扩展观测测试常量
Verify / test (pull_request) Successful in 5m43s
Verify / lint (pull_request) Successful in 6m3s
2026-09-21 07:33:10 +00:00
panxiao81 5a7b38ad26 feat: 接入 Instance 扩展能力观测
Verify / test (pull_request) Successful in 4m16s
Verify / lint (pull_request) Failing after 4m10s
2026-09-21 06:32:40 +00:00
panxiao81 8ac6283573 feat: 迁移 Database Instance 领域基线 2026-09-21 06:32:40 +00:00
panxiao81 33fb3ec972 Merge pull request 'docs: 明确 Ayatori 控制面与产品范围' (#3) from docs/ayatori-architecture into main
Verify / test (push) Successful in 4m39s
Verify / lint (push) Successful in 3m47s
2026-09-21 06:32:19 +00:00
panxiao81 86953cd91a docs: 对齐产品里程碑与集成验证要求
Verify / test (pull_request) Successful in 3m14s
Verify / lint (pull_request) Successful in 3m45s
2026-09-21 05:21:22 +00:00
panxiao81 ddd717209e chore: 添加 homelab 知识维护 skill
Verify / test (pull_request) Failing after 36s
Verify / lint (pull_request) Failing after 39s
2026-09-21 05:14:04 +00:00
panxiao81 2756803ba4 docs: 保留 DBaaS 已批准设计合同
Verify / test (pull_request) Successful in 10m35s
Verify / lint (pull_request) Successful in 12m9s
2026-09-20 20:38:53 +00:00
panxiao81 76752c8443 docs: 统一 Database API 到 Ayatori 域 2026-09-20 20:38:53 +00:00
panxiao81 36138f835a docs: 允许 Database 模块无兼容负担重构 2026-09-20 20:38:52 +00:00
panxiao81 de8b7f9b04 docs: 记录 Compute 方向与 Database 模块合并 2026-09-20 20:38:51 +00:00
panxiao81 e6b9980b3e docs: 解耦内置 API 与上游实现组件 2026-09-20 20:38:50 +00:00
panxiao81 641db531a8 docs: 按实际管理缺口限定产品范围 2026-09-20 20:38:49 +00:00
panxiao81 7b841d7dba docs: 明确 API machinery 与领域控制循环边界 2026-09-20 20:38:48 +00:00
panxiao81 af001b6188 Merge pull request 'feat: scaffold job execution API' (#1) from feat/job-api-scaffold into main
Verify / test (push) Successful in 6m6s
Verify / lint (push) Successful in 6m40s
Reviewed-on: #1
2026-09-18 18:00:43 +00:00
panxiao81 7a972afaa6 test: ignore invalid embedlit suggestions
Verify / test (pull_request) Successful in 5m44s
Verify / lint (pull_request) Successful in 6m15s
2026-09-18 16:28:53 +00:00
panxiao81 5bd0455940 test: satisfy Go 1.27 modernization lint
Verify / test (pull_request) Failing after 5m11s
Verify / lint (pull_request) Failing after 6m5s
2026-09-17 19:06:39 +00:00
panxiao81 4846ff2aba feat: scaffold job execution API
Verify / lint (pull_request) Failing after 5m37s
Verify / test (pull_request) Successful in 5m38s
2026-09-17 18:58:11 +00:00
panxiao81 7a47221280 docs: design job execution APIs 2026-09-17 18:29:17 +00:00
panxiao81 f559ffebf9 docs: define ephemeral job retention 2026-09-17 17:42:48 +00:00
panxiao81 fd643da0ca docs: define modular controller boundaries 2026-09-17 17:16:54 +00:00
184 changed files with 19675 additions and 42 deletions
+76
View File
@@ -0,0 +1,76 @@
---
name: homelab-knowledge
description: Query and maintain the shared homelab-wiki when working on homelab services, infrastructure, architecture, operations, or current service status. Use it to gather existing context before work and to keep durable knowledge synchronized after relevant changes; do not use it for unrelated software work or as a substitute for commit and PR history.
---
# Homelab Knowledge
Use `homelab-wiki` as the shared long-lived knowledge base for people and agents. Search it directly with `rg`; do not introduce a search index, vector database, or generated copy of the wiki.
## Locate the wiki
Resolve the checkout in this order:
1. `$HOMELAB_WIKI_PATH`, when set.
2. A sibling directory named `homelab-wiki` next to the current repository.
3. `/home/panxiao81/homelab-wiki` when it exists.
If no checkout is available, report that constraint. Do not silently skip the knowledge step, clone a repository, or create a replacement wiki without the user's authorization.
Before using the wiki, read its `AGENTS.md` completely. For edits, also read `README.md` and `CONTRIBUTING.md` completely and follow any more specific instructions associated with the target page.
## Gather context
At the beginning of a homelab task:
1. Derive search terms from the component name, service aliases, hostnames, Kubernetes resources, configuration keys, error text, and task intent.
2. Use `rg -n -i` in the wiki to find candidate pages. Prefer several precise searches over reading the whole repository.
3. Follow the wiki's task index, service index, architecture constraints, source records, and verification conflicts when they are relevant.
4. Read the closest authoritative pages and their material links before making decisions. Also read the corresponding source repository README or runbook when changing an implementation.
5. Distinguish documented design, declared configuration, deployment history, live verification, and work currently in progress. Do not present one as another.
For questions about current project or service status, first obtain the maintainer's current-work and ticket context as required by the wiki, unless the conversation already provides that authorization and scope. Reading documentation does not authorize live-system inspection.
Answer read-only questions from the evidence found. Include paths or links that let the user verify important claims, and state when evidence may be stale or conflicting.
## Maintain knowledge after changes
For any code, configuration, infrastructure, or operational change, perform a documentation-impact check before declaring the task complete.
Update the wiki in the same task when the change affects durable knowledge such as:
- service purpose, lifecycle, entry point, authentication, permissions, dependencies, or first-use path;
- architecture boundaries or accepted constraints;
- deployment ownership or persistent operating behavior;
- troubleshooting, recovery, verification, or maintenance procedures;
- the addition, replacement, or retirement of a service.
Keep one-time progress, implementation narration, and release-by-release history in commits, PRs, or tickets. Do not copy them into the wiki unless they change a durable stage summary. Implementation-specific parameters may remain in the source repository README or runbook when the wiki convention says to link rather than duplicate them.
When editing:
1. Inspect both the source-repository diff and the wiki working tree before writing. Preserve unrelated user changes in both repositories.
2. Update the page closest to the fact first, then only the navigation, indexes, constraints, or verification records that the wiki rules require.
3. Preserve evidence metadata. Never advance `last_verified` without performing the stated live verification; ordinary review may update only fields permitted by the wiki.
4. Link related source commits, PRs, or paths when available. Clearly mark uncommitted sources and unfinished cross-repository synchronization.
5. Record conflicts rather than resolving them by assumption. Ask before live inspection or before choosing among materially conflicting current-state claims.
6. Keep credentials, tokens, private keys, Terraform state, secret values, and sensitive command output out of documentation. Never read or copy known sensitive files merely to improve the wiki.
Wiki edits are a separate repository change. Do not commit, push, open a PR, or modify a live system unless the user has authorized that action.
## Verify and report
After editing the wiki, run from its root:
```bash
python3 scripts/check_docs.py
git diff --check
```
If the checker itself changed, also run:
```bash
python3 -m unittest discover -s tests -v
```
In the final response, report source-repository changes and wiki changes separately, including validation performed and anything still awaiting verification or cross-repository linkage. If no wiki update was needed, state the concrete reason; do not merely say that documentation was unaffected.
+11
View File
@@ -0,0 +1,11 @@
# This file configures golangci-lint with module plugins.
# When you run 'make lint', it will automatically build a custom golangci-lint binary
# with all the plugins listed below.
#
# See: https://golangci-lint.run/plugins/module-plugins/
version: v2.13.1
plugins:
# logcheck validates structured logging calls and parameters (e.g., balanced key-value pairs)
- module: "sigs.k8s.io/logtools"
import: "sigs.k8s.io/logtools/logcheck/gclplugin"
version: latest
+14
View File
@@ -0,0 +1,14 @@
# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file
# Ignore everything by default and re-include only needed files
**
# Re-include Go source files (but not *_test.go)
# If you use Podman, re-include your source directories by name,
# such as !cmd, !api, and !internal.
# See https://github.com/containers/buildah/issues/6417
!**/*.go
**/*_test.go
# Re-include Go module files
!go.mod
!go.sum
+76
View File
@@ -0,0 +1,76 @@
name: Verify
on:
# 合并前完成全量验证,合并到 main 后不重复运行同一套检查。
pull_request:
workflow_dispatch:
jobs:
test:
runs-on: [self-hosted, pod]
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417
with:
go-version-file: go.mod
cache: true
- name: Verify generated files and tests
run: |
make test
git diff --exit-code
- name: Build controller entrypoint
run: |
make build
./bin/manager --help
lint:
runs-on: [self-hosted, pod]
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417
with:
go-version-file: go.mod
cache: true
- name: Lint
run: |
make lint
make lint-database-integration
database-integration:
runs-on: [self-hosted, pod]
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417
with:
go-version-file: go.mod
cache: true
# Runner 提供本 job 可用的 Docker;workflow 只验证,不重复启动 daemon。
- name: Verify Docker availability
shell: bash
run: |
set -euo pipefail
docker version
docker info --format 'Server={{.ServerVersion}} StorageDriver={{.Driver}}'
- name: Test Database integration with real backends
run: make test-database-integration
+1
View File
@@ -2,6 +2,7 @@
/bin/
/dist/
/coverage/
/cover.out
# Local configuration and credentials
.env
+68
View File
@@ -0,0 +1,68 @@
version: "2"
run:
allow-parallel-runners: true
linters:
default: none
enable:
- copyloopvar
- depguard
- dupl
- errcheck
- ginkgolinter
- goconst
- gocyclo
- govet
- ineffassign
- lll
- modernize
- misspell
- nakedret
- prealloc
- revive
- staticcheck
- unconvert
- unparam
- unused
- logcheck
settings:
custom:
logcheck:
type: "module"
description: Checks Go logging calls for Kubernetes logging conventions.
depguard:
rules:
forbid-sort-pkg:
deny:
- pkg: sort
desc: Should be replaced with slices package
revive:
rules:
- name: comment-spacings
- name: import-shadowing
modernize:
disable:
- omitzero
exclusions:
generated: lax
rules:
- linters:
- lll
path: api/*
- linters:
- dupl
- lll
path: internal/*
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
+39
View File
@@ -3,8 +3,47 @@
- 本仓库是 ddupan.top homelab 的内部基础设施控制平面,不以通用发行版为初期目标。
- 提交、文档和代码注释优先使用中文;公共 API 标识符和代码遵循对应语言惯例。
- 不要重新实现已有成熟后端的核心能力;新增实现前先确认能否通过稳定 API 进行薄适配。
- 不要按传统私有云或公有云产品清单推导 Ayatori 应实现的资源。新增北向 API 前必须证明 homelab
存在真实、重复的管理缺口,现有成熟 API/IaC 不能提供足够的生命周期、状态或权限体验;“后端
能做到”或“其他云平台提供”本身不是产品需求。
- 当前已确认的首要产品方向是 Database、LoadBalancer、Bucket/Object Storage;VirtualMachine
也具有明确价值,但南向实现较重。Run/Job 是验证 controller 与 adapter 的内部执行切片,不应
自动演化为 FaaS、Cloud Run 或应用托管产品。KaaS 仅在出现真实需求时评估,不是必达终点。
- Ayatori 复用 Kubernetes 的核心目标是 API machinery:对象存储与并发控制、list/watch、
informer、RBAC、admission、版本化 API 和审计;不要据此推断 Ayatori 是 Kubernetes
workload 平台,也不要默认复用 Kubernetes 的调度与数据面语义。
- 复用 Kubernetes 内置资源只表示采用其 API contract,不表示必须运行或模拟上游实现组件。
例如 Ayatori Compute Agent 可以直接实现 `core/v1 Node` 与 Lease 的状态语义,Ayatori
controller 可以自行消费 Node;不得仅因使用 Node 推导必须引入 kubelet、Pod、CRI、
kube-scheduler 或 kube-controller-manager。对每个复用资源分别明确 producer、consumer、
ownership 与实际采用的字段语义。
- kube-apiserver 是 Ayatori 的 API 与状态协调平面,不是领域调度器。资源的调度、生命周期、
故障恢复、垃圾回收和后端收敛由 Ayatori controllers 实现;新增能力前应明确其属于 API
machinery、Ayatori 领域控制循环还是外部 backend,避免把职责放错层。
- Kubernetes、OpenSandbox、Proxmox 等均是 Ayatori 的可替换 backend/executor。除管理组件自身
的部署外,不得仅因 controller 运行在 Kubernetes 中,就把原生 Pod、Job、Service、
NetworkPolicy、owner reference 或同 namespace 行为作为领域 API 的隐含语义;需要这些能力时
必须由 adapter 契约显式表达,并考虑后端位于其他集群或完全不是 Kubernetes 的情况。
- 不要以减少自有 controller 数量为目的引入 generic-apiserver、聚合 API Server 或自行实现
API Server。只有 CRD/kube-apiserver 在存储、API 语义或扩展能力上形成已验证的阻碍时,才评估
接管 watch、RBAC、版本兼容和存储迁移等复杂度;controller 工作本身不会因此消失。
- 在自行设计通用控制循环、资源生命周期、调度、回收或故障恢复机制前,先调查 Kubernetes
核心及成熟开源 controller/operator 的实现;优先复用经过验证的模式,并记录有意偏离的
理由。
- 不要引入统一包装所有能力的 Application CRD;应用应直接组合正交的平台资源。
- Proxmox VM 的北向管理不能假定单一 API 覆盖完整生命周期。允许按能力组合 Proxmox API、节点
上的受限强类型 Agent/CLI 操作和 ManualTask;节点 Agent 不得退化为无版本契约的任意远程 shell。
- 所有 controller 必须考虑幂等、observe、finalizer、conditions、删除策略和恢复行为。
- Ayatori 会联动 Kubernetes API、虚拟化、存储、网络及其他外部控制面;集成测试是功能完成
标准的一部分,不得仅凭 fake client 或 mock 测试宣告 controller、adapter 或生命周期变更完成。
- 测试应按风险分层:纯领域规则使用快速单元测试;API schema、CEL、status subresource、
watch/cache、owner reference 和 reconcile 事件链使用 envtest;需要 scheduler、kubelet、网络、
存储或真实后端行为的路径在 Dev 集群或对应后端环境执行端到端测试。
- fake client 适合穷举状态机和错误分支,但它不会完整执行 API server defaulting、validation、
resourceVersion、garbage collection 或新版 Kubernetes 约束;涉及这些语义时必须增加真实 API
server 测试。跨 adapter 的共同契约应使用同一套 contract tests,避免各实现产生语义漂移。
- 集成测试必须覆盖正常路径以及幂等重试、controller 重启、依赖稍后出现、删除/finalizer、
后端结果不确定和并发竞态等恢复路径;无法在当前层测试的部分要明确记录由哪一层验证。
- Secret、token、kubeconfig 及具体生产凭据不得提交到仓库。
- `deploy/dev/` 与 `deploy/prod/` 使用相同制品;生产版本只通过 promotion 更新。
- 内部专用不构成降低测试、版本、恢复、安全和可审计要求的理由。
+33
View File
@@ -0,0 +1,33 @@
# Build the manager binary
# Override BASE_IMAGE to build from another registry, e.g. docker.io/library/golang:1.27.1
ARG BASE_IMAGE=golang:1.27.1
FROM ${BASE_IMAGE} AS builder
ARG TARGETOS
ARG TARGETARCH
WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
RUN go mod download
# Copy the Go source (relies on .dockerignore to filter)
COPY . .
# Build
# the GOARCH has no default value to allow the binary to be built according to the host where the command
# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO
# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore,
# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform.
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go
# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532
ENTRYPOINT ["/manager"]
+239
View File
@@ -0,0 +1,239 @@
# Image URL to use all building/pushing image targets
IMG ?= controller:latest
# YEAR defines the year value used for substituting the YEAR placeholder in the boilerplate header.
YEAR ?= $(shell date +%Y)
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN))
GOBIN=$(shell go env GOPATH)/bin
else
GOBIN=$(shell go env GOBIN)
endif
# CONTAINER_TOOL defines the container tool to be used for building images.
# Be aware that the target commands are only tested with Docker which is
# scaffolded by default. However, you might want to replace it to use other
# tools. (i.e. podman)
CONTAINER_TOOL ?= docker
# Setting SHELL to bash allows bash commands to be executed by recipes.
# Options are set to exit when a recipe line exits non-zero or a piped command fails.
SHELL = /usr/bin/env bash -o pipefail
.SHELLFLAGS = -ec
.PHONY: all
all: build
##@ General
# The help target prints out all targets with their descriptions organized
# beneath their categories. The categories are represented by '##@' and the
# target descriptions by '##'. The awk command is responsible for reading the
# entire set of makefiles included in this invocation, looking for lines of the
# file as xyz: ## something, and then pretty-format the target and help. Then,
# if there's a line with ##@ something, that gets pretty-printed as a category.
# More info on the usage of ANSI control characters for terminal formatting:
# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
# More info on the awk command:
# http://linuxcommand.org/lc3_adv_awk.php
.PHONY: help
help: ## Display this help.
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
##@ Development
.PHONY: manifests
manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
"$(CONTROLLER_GEN)" rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
.PHONY: generate
generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
"$(CONTROLLER_GEN)" object paths="./..."
.PHONY: fmt
fmt: ## Run go fmt against code.
go fmt ./...
.PHONY: vet
vet: ## Run go vet against code.
go vet ./...
.PHONY: test
test: manifests generate fmt vet setup-envtest ## Run tests.
KUBEBUILDER_ASSETS="$(shell "$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path)" go test ./... -coverprofile cover.out
.PHONY: lint
lint: golangci-lint ## Run golangci-lint linter
"$(GOLANGCI_LINT)" run
.PHONY: test-database-integration
test-database-integration: setup-envtest ## 使用临时 API server、PostgreSQL 与 OpenBao 容器验证 Database 后端。
KUBEBUILDER_ASSETS="$(shell "$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path)" \
go test -tags=integration -race -count=1 ./internal/database/... ./internal/infra/... ./internal/bootstrap/...
.PHONY: lint-database-integration
lint-database-integration: golangci-lint ## 检查集成测试构建标签下的 Database 代码。
"$(GOLANGCI_LINT)" run --build-tags=integration \
./internal/database/... ./internal/infra/... ./internal/bootstrap/...
.PHONY: lint-fix
lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes
"$(GOLANGCI_LINT)" run --fix
.PHONY: lint-config
lint-config: golangci-lint ## Verify golangci-lint linter configuration
"$(GOLANGCI_LINT)" config verify
##@ Build
.PHONY: build
build: manifests generate fmt vet ## Build manager binary.
go build -o bin/manager cmd/main.go
.PHONY: run
run: manifests generate fmt vet ## Run a controller from your host.
go run ./cmd/main.go
# If you wish to build the manager image targeting other platforms you can use the --platform flag.
# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it.
# More info: https://docs.docker.com/develop/develop-images/build_enhancements/
# Override BASE_IMAGE to build from another registry, e.g.
# make docker-build IMG=<img> BASE_IMAGE=docker.io/library/golang:1.27.1
.PHONY: docker-build
docker-build: ## Build docker image with the manager.
$(CONTAINER_TOOL) build $(if $(BASE_IMAGE),--build-arg BASE_IMAGE=$(BASE_IMAGE)) -t ${IMG} .
.PHONY: docker-push
docker-push: ## Push docker image with the manager.
$(CONTAINER_TOOL) push ${IMG}
# PLATFORMS defines the target platforms for the manager image be built to provide support to multiple
# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to:
# - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/
# - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/
# - be able to push the image to your registry (i.e. if you do not set a valid value via IMG=<myregistry/image:<tag>> then the export will fail)
# To adequately provide solutions that are compatible with multiple platforms, you should consider using this option.
PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le
.PHONY: docker-buildx
docker-buildx: ## Build and push docker image for the manager for cross-platform support
# copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile
sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross
- $(CONTAINER_TOOL) buildx create --name ayatori-builder
$(CONTAINER_TOOL) buildx use ayatori-builder
- $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) $(if $(BASE_IMAGE),--build-arg BASE_IMAGE=$(BASE_IMAGE)) --tag ${IMG} -f Dockerfile.cross .
- $(CONTAINER_TOOL) buildx rm ayatori-builder
rm Dockerfile.cross
.PHONY: build-installer
build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment.
mkdir -p dist
cd config/manager && "$(KUSTOMIZE)" edit set image controller=${IMG}
"$(KUSTOMIZE)" build config/default > dist/install.yaml
##@ Deployment
ifndef ignore-not-found
ignore-not-found = false
endif
.PHONY: install
install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config.
@out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \
if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" apply -f -; else echo "No CRDs to install; skipping."; fi
.PHONY: uninstall
uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
@out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \
if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f -; else echo "No CRDs to delete; skipping."; fi
.PHONY: deploy
deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
cd config/manager && "$(KUSTOMIZE)" edit set image controller=${IMG}
"$(KUSTOMIZE)" build config/default | "$(KUBECTL)" apply -f -
.PHONY: undeploy
undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
"$(KUSTOMIZE)" build config/default | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f -
##@ Dependencies
## Location to install dependencies to
LOCALBIN ?= $(shell pwd)/bin
$(LOCALBIN):
mkdir -p "$(LOCALBIN)"
## Tool Binaries
KUBECTL ?= kubectl
KUSTOMIZE ?= $(LOCALBIN)/kustomize
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
ENVTEST ?= $(LOCALBIN)/setup-envtest
GOLANGCI_LINT = $(LOCALBIN)/golangci-lint
## Tool Versions
KUSTOMIZE_VERSION ?= v5.8.1
CONTROLLER_TOOLS_VERSION ?= v0.22.0
#ENVTEST_VERSION is the controller-runtime version to use for setup-envtest, derived from go.mod
ENVTEST_VERSION ?= $(shell v='$(call gomodver,sigs.k8s.io/controller-runtime)'; \
[ -n "$$v" ] || { echo "Set ENVTEST_VERSION manually (controller-runtime replace has no tag)" >&2; exit 1; }; \
printf '%s\n' "$$v")
#ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31)
ENVTEST_K8S_VERSION ?= $(shell v='$(call gomodver,k8s.io/api)'; \
[ -n "$$v" ] || { echo "Set ENVTEST_K8S_VERSION manually (k8s.io/api replace has no tag)" >&2; exit 1; }; \
printf '%s\n' "$$v" | sed -E 's/^v?[0-9]+\.([0-9]+).*/1.\1/')
GOLANGCI_LINT_VERSION ?= v2.13.1
.PHONY: kustomize
kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary.
$(KUSTOMIZE): $(LOCALBIN)
$(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION))
.PHONY: controller-gen
controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary.
$(CONTROLLER_GEN): $(LOCALBIN)
$(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION))
.PHONY: setup-envtest
setup-envtest: envtest ## Download the binaries required for ENVTEST in the local bin directory.
@echo "Setting up envtest binaries for Kubernetes version $(ENVTEST_K8S_VERSION)..."
@"$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path || { \
echo "Error: Failed to set up envtest binaries for version $(ENVTEST_K8S_VERSION)."; \
exit 1; \
}
.PHONY: envtest
envtest: $(ENVTEST) ## Download setup-envtest locally if necessary.
$(ENVTEST): $(LOCALBIN)
$(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION))
.PHONY: golangci-lint
golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary.
$(GOLANGCI_LINT): $(LOCALBIN)
$(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION))
@test -f .custom-gcl.yml && { \
echo "Building custom golangci-lint with plugins..." && \
$(GOLANGCI_LINT) custom --destination $(LOCALBIN) --name golangci-lint-custom && \
mv -f $(LOCALBIN)/golangci-lint-custom $(GOLANGCI_LINT); \
} || true
# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist
# $1 - target path with name of binary
# $2 - package url which can be installed
# $3 - specific version of package
define go-install-tool
@[ -f "$(1)-$(3)" ] && [ "$$(readlink -- "$(1)" 2>/dev/null)" = "$(1)-$(3)" ] || { \
set -e; \
package=$(2)@$(3) ;\
echo "Downloading $${package}" ;\
rm -f "$(1)" ;\
GOBIN="$(LOCALBIN)" go install $${package} ;\
mv "$(LOCALBIN)/$$(basename "$(1)")" "$(1)-$(3)" ;\
} ;\
ln -sf "$$(realpath "$(1)-$(3)")" "$(1)"
endef
define gomodver
$(shell go list -m -f '{{if .Replace}}{{.Replace.Version}}{{else}}{{.Version}}{{end}}' $(1) 2>/dev/null)
endef
+42
View File
@@ -0,0 +1,42 @@
# Code generated by tool. DO NOT EDIT.
# This file is used to track the info used to scaffold your project
# and allow the plugins properly work.
# More info: https://book.kubebuilder.io/reference/project-config.html
cliVersion: 4.16.0
domain: ayatori.ddupan.top
layout:
- go.kubebuilder.io/v4
multigroup: true
projectName: ayatori
repo: git.ddupan.top/panxiao81/ayatori
resources:
- api:
crdVersion: v1
namespaced: true
domain: ayatori.ddupan.top
group: execution
kind: Job
path: git.ddupan.top/panxiao81/ayatori/api/execution/v1alpha1
version: v1alpha1
- api:
crdVersion: v1
domain: ayatori.ddupan.top
group: execution
kind: JobClass
path: git.ddupan.top/panxiao81/ayatori/api/execution/v1alpha1
version: v1alpha1
- api:
crdVersion: v1
domain: ayatori.ddupan.top
group: execution
kind: KubernetesExecutionParameters
path: git.ddupan.top/panxiao81/ayatori/api/execution/v1alpha1
version: v1alpha1
- api:
crdVersion: v1
domain: ayatori.ddupan.top
group: execution
kind: OpenSandboxExecutionParameters
path: git.ddupan.top/panxiao81/ayatori/api/execution/v1alpha1
version: v1alpha1
version: "3"
+8 -3
View File
@@ -28,11 +28,16 @@ Ayatori 是 `ddupan.top` homelab 的内部基础设施控制平面。它以 Kube
- [执行模型](docs/concepts/execution-model.md)
- [环境与发布](docs/concepts/environments.md)
- [路线图](docs/roadmap.md)
- [ADR-0001:采用 Kubernetes API 作为资源模型](docs/decisions/0001-kubernetes-api-machinery.md)
- [ADR-0001:采用 Kubernetes API machinery 作为状态协调平面](docs/decisions/0001-kubernetes-api-machinery.md)
- [ADR-0002:采用 k0s 与可选工作负载运行时](docs/decisions/0002-k0s-optional-workload-runtime.md)
- [ADR-0003:直接连接 Dev API 的开发循环](docs/decisions/0003-dev-api-development-loop.md)
- [ADR-0006:按实际管理缺口扩展资源 API](docs/decisions/0006-demand-driven-resource-scope.md)
- [ADR-0007:复用 Node API 建立按需实现的 Compute 能力](docs/decisions/0007-compute-node-and-vm-boundary.md)
- [ADR-0008:将 PostgreSQL Tenant Operator 合并为 Ayatori Database 模块](docs/decisions/0008-merge-postgresql-tenant-operator.md)
- [ADR-0009:分离 Database 资源与 Tenant 申请](docs/decisions/0009-database-resource-and-claim.md)
## 当前状态
Ayatori 处于设计与早期实现阶段。第一个纵向切片计划是统一 Job API 与 Kubernetes
Pod executor,随后接入 OpenSandbox executor。
Ayatori 处于设计与早期实现阶段。当前使用 Job controller 验证第一个完整控制循环与 adapter
边界;它不是通用 Job Service 或 FaaS 产品承诺。首批实际产品方向是 Database、LoadBalancer
和 Bucket/Object Storage,具体顺序按纵向价值决定。
+67
View File
@@ -0,0 +1,67 @@
package v1alpha1
import "k8s.io/apimachinery/pkg/types"
// ObjectName 定位集群级资源,不携带 namespace 或隐式跨 API group 引用。
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$`
type ObjectName string
// PostgreSQLIdentifier 是第一版受管 database 与 login role 使用的名称。
// +kubebuilder:validation:MaxLength=63
// +kubebuilder:validation:Pattern=`^[a-z][a-z0-9_]{0,62}$`
type PostgreSQLIdentifier string
// InstanceReference 仅引用同 API group 的集群级 PostgreSQLInstance。
type InstanceReference struct {
Name ObjectName `json:"name"`
}
// DatabaseReference 是 Tenant 对已有集群级 PostgreSQLDatabase 的选择。
type DatabaseReference struct {
Name ObjectName `json:"name"`
}
// BoundDatabaseReference 记录已经参与绑定的对象身份,而非仅记录可复用的名称。
type BoundDatabaseReference struct {
Name ObjectName `json:"name"`
// +kubebuilder:validation:Type=string
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=128
UID types.UID `json:"uid"`
}
// TenantReference 是 Database 的当前绑定记录,不是允许绑定名单。
type TenantReference struct {
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=63
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`
Namespace string `json:"namespace"`
Name ObjectName `json:"name"`
// +kubebuilder:validation:Type=string
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=128
UID types.UID `json:"uid"`
}
// ReclaimPolicy 控制资源释放后的处置,只有资源管理者可以修改。
// +kubebuilder:validation:Enum=Retain;Delete
type ReclaimPolicy string
const (
ReclaimRetain ReclaimPolicy = "Retain"
ReclaimDelete ReclaimPolicy = "Delete"
)
// CredentialReference 定位 OpenBao KV v2 凭据,不包含任何秘密值。
// 管理员在导入声明中指定,controller 在 status 中固定位置;两者均须检查部署允许的范围。
type CredentialReference struct {
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
Mount string `json:"mount"`
// Path 是 mount 内的逻辑路径,不含 KV v2 的 data/ API 前缀。
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=1024
Path string `json:"path"`
}
@@ -0,0 +1,87 @@
package v1alpha1_test
import (
"testing"
databasev1alpha1 "git.ddupan.top/panxiao81/ayatori/api/database/v1alpha1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
)
func testCredentialStatus(t *testing.T, client ctrlclient.Client) {
database := validDatabase("credential-status")
if err := client.Create(t.Context(), database); err != nil {
t.Fatal(err)
}
withoutLocation := database.DeepCopy()
withoutLocation.Status.CredentialVersion = 1
if err := client.Status().Update(t.Context(), withoutLocation); !apierrors.IsInvalid(err) {
t.Fatalf("没有位置不能确认版本: %v", err)
}
database.Status.CredentialRef = &databasev1alpha1.CredentialReference{
Mount: "application-secrets", Path: "applications/" + string(database.UID),
}
if err := client.Status().Update(t.Context(), database); err != nil {
t.Fatal(err)
}
testCredentialStatusRemoval(t, client, database)
changedTarget := database.DeepCopy()
changedTarget.Spec.Database = "another_database"
if err := client.Update(t.Context(), changedTarget); !apierrors.IsInvalid(err) {
t.Fatalf("凭据位置固定后不得更换实际目标: %v", err)
}
stale := database.DeepCopy()
database.Status.CredentialVersion = 1
if err := client.Status().Update(t.Context(), database); err != nil {
t.Fatal(err)
}
stale.Status.Phase = "Binding"
if err := client.Status().Update(t.Context(), stale); !apierrors.IsConflict(err) {
t.Fatalf("旧 resourceVersion 不得覆盖确认结果: %v", err)
}
testCredentialStatusRemoval(t, client, database)
for _, version := range []int64{0, -1, 2} {
changed := database.DeepCopy()
changed.Status.CredentialVersion = version
if err := client.Status().Update(t.Context(), changed); !apierrors.IsInvalid(err) {
t.Fatalf("不可移除或更改确认版本 %d: %v", version, err)
}
}
database.Status.Conditions = []metav1.Condition{{
Type: "CredentialsReady", Status: metav1.ConditionFalse,
Reason: "DependencyUnavailable", Message: "凭据暂时无法读取",
ObservedGeneration: database.Generation, LastTransitionTime: metav1.Now(),
}}
if err := client.Status().Update(t.Context(), database); err != nil {
t.Fatalf("当前不可用不应阻止保留确认记录: %v", err)
}
reloaded := &databasev1alpha1.PostgreSQLDatabase{}
if err := client.Get(t.Context(), ctrlclient.ObjectKeyFromObject(database), reloaded); err != nil {
t.Fatal(err)
}
if reloaded.Status.CredentialVersion != 1 || reloaded.Status.CredentialRef == nil {
t.Fatal("重读丢失凭据确认记录")
}
}
func testCredentialStatusRemoval(t *testing.T, client ctrlclient.Client, database *databasev1alpha1.PostgreSQLDatabase) {
t.Helper()
for _, change := range []struct {
name string
edit func(*databasev1alpha1.PostgreSQLDatabase)
}{
{"移除整个 status", func(d *databasev1alpha1.PostgreSQLDatabase) { d.Status = databasev1alpha1.PostgreSQLDatabaseStatus{} }},
{"移除位置", func(d *databasev1alpha1.PostgreSQLDatabase) { d.Status.CredentialRef = nil }},
{"修改 mount", func(d *databasev1alpha1.PostgreSQLDatabase) { d.Status.CredentialRef.Mount = "other" }},
{"修改 path", func(d *databasev1alpha1.PostgreSQLDatabase) { d.Status.CredentialRef.Path = "applications/other" }},
} {
t.Run(change.name, func(t *testing.T) {
changed := database.DeepCopy()
change.edit(changed)
if err := client.Status().Update(t.Context(), changed); !apierrors.IsInvalid(err) {
t.Fatalf("不应接受已固定凭据位置的更改: %v", err)
}
})
}
}
@@ -0,0 +1,25 @@
// Package v1alpha1 定义 Database 领域的 Kubernetes API。
// +kubebuilder:object:generate=true
// +groupName=database.ayatori.ddupan.top
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
var (
SchemeGroupVersion = schema.GroupVersion{Group: "database.ayatori.ddupan.top", Version: "v1alpha1"}
GroupVersion = SchemeGroupVersion
SchemeBuilder = runtime.NewSchemeBuilder(func(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&PostgreSQLInstance{}, &PostgreSQLInstanceList{},
&PostgreSQLDatabase{}, &PostgreSQLDatabaseList{},
&PostgreSQLTenant{}, &PostgreSQLTenantList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
})
AddToScheme = SchemeBuilder.AddToScheme
)
@@ -0,0 +1,73 @@
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)
// PostgreSQLDatabaseSpec 是一库、一个 login owner 及凭据的独立资源声明。
// +kubebuilder:validation:XValidation:rule="(self.source == 'Import') == has(self.credentialRef)",message="only imported databases require an existing credentialRef"
type PostgreSQLDatabaseSpec struct {
InstanceRef InstanceReference `json:"instanceRef"`
Database PostgreSQLIdentifier `json:"database"`
LoginRole PostgreSQLIdentifier `json:"loginRole"`
// Source 明确区分创建与只读导入,不从后端同名对象推断。
// +kubebuilder:validation:Enum=Provision;Import
Source string `json:"source"`
// +optional
CredentialRef *CredentialReference `json:"credentialRef,omitempty"`
// +kubebuilder:default=Retain
// +optional
ReclaimPolicy ReclaimPolicy `json:"reclaimPolicy,omitempty"`
// TenantRef 由 controller 先写入;Released 时仍保留旧身份。
// +optional
TenantRef *TenantReference `json:"tenantRef,omitempty"`
}
type PostgreSQLDatabaseStatus struct {
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// InstanceUID 记录观察时的实例身份,不把同名新实例视为原目标。
// +optional
InstanceUID types.UID `json:"instanceUID,omitempty"`
// CredentialRef 在首次外部写入前固定凭据位置;部署配置变化不迁移此位置。
// +optional
CredentialRef *CredentialReference `json:"credentialRef,omitempty"`
// CredentialVersion 只在创建并回读成功后记录,不表示凭据当前仍可用。
// 省略表示尚未确认;已有凭据不能仅凭读取成功补记确认。
// +kubebuilder:validation:Minimum=1
// +optional
CredentialVersion int64 `json:"credentialVersion,omitempty"`
// Phase 暂不冻结供应子阶段枚举;它不是操作授权或绑定的替代记录。
// +optional
Phase string `json:"phase,omitempty"`
// +listType=map
// +listMapKey=type
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster
// +kubebuilder:validation:XValidation:rule="!has(self.status) || !has(self.status.credentialVersion) || has(self.status.credentialRef)",message="credentialVersion requires credentialRef"
// +kubebuilder:validation:XValidation:rule="!(has(oldSelf.status) && has(oldSelf.status.credentialRef)) || (has(self.status) && has(self.status.credentialRef) && self.status.credentialRef == oldSelf.status.credentialRef)",message="recorded credentialRef cannot change or be removed"
// +kubebuilder:validation:XValidation:rule="!(has(oldSelf.status) && has(oldSelf.status.credentialVersion)) || (has(self.status) && has(self.status.credentialVersion) && self.status.credentialVersion == oldSelf.status.credentialVersion)",message="confirmed credentialVersion cannot change or be removed"
// +kubebuilder:validation:XValidation:rule="!(has(oldSelf.spec.tenantRef) || (has(oldSelf.status) && (has(oldSelf.status.instanceUID) || has(oldSelf.status.credentialRef)))) || (self.spec.instanceRef == oldSelf.spec.instanceRef && self.spec.database == oldSelf.spec.database && self.spec.loginRole == oldSelf.spec.loginRole && self.spec.source == oldSelf.spec.source && has(self.spec.credentialRef) == has(oldSelf.spec.credentialRef) && (!has(oldSelf.spec.credentialRef) || self.spec.credentialRef == oldSelf.spec.credentialRef))",message="managed database target cannot change after observation or binding starts"
// +kubebuilder:printcolumn:name="Instance",type=string,JSONPath=`.spec.instanceRef.name`
// +kubebuilder:printcolumn:name="Database",type=string,JSONPath=`.spec.database`
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=='Ready')].status`
type PostgreSQLDatabase struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitzero"`
Spec PostgreSQLDatabaseSpec `json:"spec"`
// +optional
Status PostgreSQLDatabaseStatus `json:"status,omitzero"`
}
// +kubebuilder:object:root=true
type PostgreSQLDatabaseList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitzero"`
Items []PostgreSQLDatabase `json:"items"`
}
@@ -0,0 +1,80 @@
package v1alpha1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// PostgreSQLEndpoint 显式区分证书主机名与实际连接 IP,不进行 DNS 推导。
type PostgreSQLEndpoint struct {
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
Host string `json:"host"`
// +kubebuilder:validation:MaxLength=45
// +kubebuilder:validation:XValidation:rule="isIP(self)",message="hostaddr must be a single IPv4 or IPv6 address"
HostAddr string `json:"hostaddr"`
// +kubebuilder:default=5432
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=65535
// +optional
Port int32 `json:"port,omitempty"`
// +kubebuilder:default=postgres
// +optional
Database PostgreSQLIdentifier `json:"database,omitempty"`
// +kubebuilder:default=verify-full
// +kubebuilder:validation:Enum=disable;require;verify-ca;verify-full
// +optional
SSLMode string `json:"sslMode,omitempty"`
}
// AdminCredentialReference 只能读取 controller namespace 的 Secret。
type AdminCredentialReference struct {
Name ObjectName `json:"name"`
// +kubebuilder:default=username
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
// +kubebuilder:validation:Pattern=`^[-._a-zA-Z0-9]+$`
// +optional
UsernameKey string `json:"usernameKey,omitempty"`
// +kubebuilder:default=password
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
// +kubebuilder:validation:Pattern=`^[-._a-zA-Z0-9]+$`
// +optional
PasswordKey string `json:"passwordKey,omitempty"`
}
type PostgreSQLInstanceSpec struct {
Endpoint PostgreSQLEndpoint `json:"endpoint"`
AdminCredentialRef AdminCredentialReference `json:"adminCredentialRef"`
}
type PostgreSQLInstanceStatus struct {
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// +kubebuilder:validation:Enum=Pending;Validating;Ready;Deleting
// +optional
Phase string `json:"phase,omitempty"`
// +optional
PostgreSQLVersion string `json:"postgresqlVersion,omitempty"`
// +listType=map
// +listMapKey=type
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=='Ready')].status`
type PostgreSQLInstance struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitzero"`
Spec PostgreSQLInstanceSpec `json:"spec"`
// +optional
Status PostgreSQLInstanceStatus `json:"status,omitzero"`
}
// +kubebuilder:object:root=true
type PostgreSQLInstanceList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitzero"`
Items []PostgreSQLInstance `json:"items"`
}
@@ -0,0 +1,70 @@
package v1alpha1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// DatabaseProvisionRequest 仅用于动态申请,省略名称时由 controller 按 Tenant 名称解析。
type DatabaseProvisionRequest struct {
InstanceRef InstanceReference `json:"instanceRef"`
// +optional
Database PostgreSQLIdentifier `json:"database,omitempty"`
// +optional
LoginRole PostgreSQLIdentifier `json:"loginRole,omitempty"`
}
// PostgreSQLTenantSpec 显式选择动态申请或已有 Database,不重复声明来源。
// +kubebuilder:validation:XValidation:rule="has(self.provision) != has(self.databaseRef)",message="exactly one of provision and databaseRef is required"
type PostgreSQLTenantSpec struct {
// +optional
Provision *DatabaseProvisionRequest `json:"provision,omitempty"`
// +optional
DatabaseRef *DatabaseReference `json:"databaseRef,omitempty"`
// Extensions 保留后端扩展名称的原样拼写,不按 SQL identifier 限制。
// +listType=set
// +optional
Extensions []string `json:"extensions,omitempty"`
// SecretName 指定 Tenant namespace 内的投射目标,省略时使用合同约定的默认名称。
// +optional
SecretName ObjectName `json:"secretName,omitempty"`
}
type PostgreSQLTenantStatus struct {
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// DatabaseRef 只有在资源侧确认绑定后才写入。
// +optional
DatabaseRef *BoundDatabaseReference `json:"databaseRef,omitempty"`
// +optional
Phase string `json:"phase,omitempty"`
// SecretName 是已观察到的同 namespace 投射目标,不包含凭据值。
// +optional
SecretName ObjectName `json:"secretName,omitempty"`
// CredentialURL 只含 OpenBao API 位置,禁止嵌入认证信息。
// +optional
CredentialURL string `json:"credentialURL,omitempty"`
// +listType=map
// +listMapKey=type
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Namespaced
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.status) || !has(oldSelf.status.phase) || !(oldSelf.status.phase in ['Binding', 'Bound', 'Deleting']) || ((has(self.spec.provision) == has(oldSelf.spec.provision)) && (!has(oldSelf.spec.provision) || self.spec.provision == oldSelf.spec.provision) && (has(self.spec.databaseRef) == has(oldSelf.spec.databaseRef)) && (!has(oldSelf.spec.databaseRef) || self.spec.databaseRef == oldSelf.spec.databaseRef))",message="binding target cannot change after binding starts"
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.status) || !has(oldSelf.status.phase) || !(oldSelf.status.phase in ['Binding', 'Bound', 'Deleting']) || (has(self.status) && has(self.status.phase) && self.status.phase in ['Binding', 'Bound', 'Deleting'])",message="binding progress cannot return to an unbound state"
// +kubebuilder:printcolumn:name="Database",type=string,JSONPath=`.status.databaseRef.name`
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=='Ready')].status`
type PostgreSQLTenant struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitzero"`
Spec PostgreSQLTenantSpec `json:"spec"`
// +optional
Status PostgreSQLTenantStatus `json:"status,omitzero"`
}
// +kubebuilder:object:root=true
type PostgreSQLTenantList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitzero"`
Items []PostgreSQLTenant `json:"items"`
}
+313
View File
@@ -0,0 +1,313 @@
package v1alpha1_test
import (
"context"
"io"
"os"
"path/filepath"
"testing"
databasev1alpha1 "git.ddupan.top/panxiao81/ayatori/api/database/v1alpha1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/yaml"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
)
const (
testNamespace = "database-api"
testInstanceName = "shared-postgres"
readyPhase = "Ready"
)
func TestDatabaseAPI(t *testing.T) {
if os.Getenv("KUBEBUILDER_ASSETS") == "" {
t.Skip("KUBEBUILDER_ASSETS 未设置;运行 make test 执行真实 API server 测试")
}
scheme := runtime.NewScheme()
if err := databasev1alpha1.AddToScheme(scheme); err != nil {
t.Fatal(err)
}
if err := corev1.AddToScheme(scheme); err != nil {
t.Fatal(err)
}
crdPath, err := filepath.Abs("../../../config/crd/bases")
if err != nil {
t.Fatal(err)
}
environment := &envtest.Environment{CRDDirectoryPaths: []string{crdPath}, ErrorIfCRDPathMissing: true}
config, err := environment.Start()
if err != nil {
t.Fatalf("启动 envtest: %v", err)
}
t.Cleanup(func() {
if err := environment.Stop(); err != nil {
t.Errorf("停止 envtest: %v", err)
}
})
client, err := ctrlclient.New(config, ctrlclient.Options{Scheme: scheme})
if err != nil {
t.Fatal(err)
}
namespace := &corev1.Namespace{}
namespace.Name = testNamespace
if err := client.Create(t.Context(), namespace); err != nil {
t.Fatal(err)
}
t.Run("作用域和默认值", func(t *testing.T) { testDefaults(t, client) })
t.Run("拒绝非法声明", func(t *testing.T) { testInvalidDeclarations(t, client) })
t.Run("status隔离和绑定并发", func(t *testing.T) { testBindingWrites(t, client) })
t.Run("凭据位置与确认版本", func(t *testing.T) { testCredentialStatus(t, client) })
t.Run("仓库示例", func(t *testing.T) { testSamples(t, client, scheme) })
}
func testSamples(t *testing.T, client ctrlclient.Client, scheme *runtime.Scheme) {
paths := []string{
"database_v1alpha1_postgresqlinstance.yaml",
"database_v1alpha1_postgresqldatabase.yaml",
"database_v1alpha1_postgresqltenant.yaml",
}
for _, name := range paths {
t.Run(name, func(t *testing.T) {
file, err := os.Open(filepath.Join("../../../config/samples", name))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := file.Close(); err != nil {
t.Error(err)
}
})
decoder := yaml.NewYAMLOrJSONDecoder(file, 4096)
for {
var raw runtime.RawExtension
if err := decoder.Decode(&raw); err == io.EOF {
break
} else if err != nil {
t.Fatal(err)
}
object, _, err := serializer.NewCodecFactory(scheme).UniversalDeserializer().Decode(raw.Raw, nil, nil)
if err != nil {
t.Fatal(err)
}
resource, ok := object.(ctrlclient.Object)
if !ok {
t.Fatalf("示例不是资源对象: %T", object)
}
if resource.GetNamespace() != "" {
resource.SetNamespace(testNamespace)
}
if err := client.Create(t.Context(), resource); err != nil {
t.Fatalf("示例未通过 API 校验: %v", err)
}
}
})
}
}
func testDefaults(t *testing.T, client ctrlclient.Client) {
instance := validInstance("defaults")
if err := client.Create(t.Context(), instance); err != nil {
t.Fatal(err)
}
endpoint := instance.Spec.Endpoint
if endpoint.Port != 5432 || endpoint.Database != "postgres" || endpoint.SSLMode != "verify-full" {
t.Fatalf("连接默认值不符: %+v", endpoint)
}
credentials := instance.Spec.AdminCredentialRef
if credentials.UsernameKey != "username" || credentials.PasswordKey != "password" {
t.Fatal("管理 Secret 字段默认值不符")
}
database := validDatabase("defaults")
if err := client.Create(t.Context(), database); err != nil {
t.Fatal(err)
}
if database.Spec.ReclaimPolicy != databasev1alpha1.ReclaimRetain {
t.Fatalf("默认回收策略 = %q", database.Spec.ReclaimPolicy)
}
// 进入删除流程前允许双向修改策略,不要求第二次审批字段。
for _, policy := range []databasev1alpha1.ReclaimPolicy{databasev1alpha1.ReclaimDelete, databasev1alpha1.ReclaimRetain} {
database.Spec.ReclaimPolicy = policy
if err := client.Update(t.Context(), database); err != nil {
t.Fatalf("修改回收策略: %v", err)
}
}
objects := []struct {
object ctrlclient.Object
namespaced bool
}{
{instance, false}, {database, false}, {validTenant("scope"), true},
}
for _, item := range objects {
namespaced, err := client.IsObjectNamespaced(item.object)
if err != nil || namespaced != item.namespaced {
t.Fatalf("%T 作用域 = %v, error = %v", item.object, namespaced, err)
}
}
// 导入不要求 Tenant 或 Instance 对象已经存在,跨对象就绪由 controller 判断。
imported := validDatabase("imported")
imported.Spec.Source = "Import"
imported.Spec.CredentialRef = &databasev1alpha1.CredentialReference{Mount: "secret", Path: "existing/app"}
if err := client.Create(t.Context(), imported); err != nil {
t.Fatal(err)
}
for _, name := range []string{"first", "second"} {
tenant := validTenant(name)
tenant.Spec.Provision = nil
tenant.Spec.DatabaseRef = &databasev1alpha1.DatabaseReference{Name: "imported"}
if err := client.Create(t.Context(), tenant); err != nil {
t.Fatalf("声明已有资源申请: %v", err)
}
}
// 两个申请都可被 API 接受,不代表二者都已绑定或获得凭据。
}
func testInvalidDeclarations(t *testing.T, client ctrlclient.Client) {
instanceCases := []struct {
name string
mutate func(*databasev1alpha1.PostgreSQLInstance)
}{
{"port", func(i *databasev1alpha1.PostgreSQLInstance) { i.Spec.Endpoint.Port = -1 }},
{"address", func(i *databasev1alpha1.PostgreSQLInstance) { i.Spec.Endpoint.HostAddr = "localhost" }},
{"scoped-address", func(i *databasev1alpha1.PostgreSQLInstance) { i.Spec.Endpoint.HostAddr = "fe80::1%eth0" }},
{"tls", func(i *databasev1alpha1.PostgreSQLInstance) { i.Spec.Endpoint.SSLMode = "prefer" }},
{"identifier", func(i *databasev1alpha1.PostgreSQLInstance) { i.Spec.Endpoint.Database = "bad-name" }},
{"secret-key", func(i *databasev1alpha1.PostgreSQLInstance) { i.Spec.AdminCredentialRef.PasswordKey = "bad/key" }},
}
for _, tc := range instanceCases {
t.Run(tc.name, func(t *testing.T) {
object := validInstance(tc.name)
tc.mutate(object)
requireInvalidCreate(t, client, object)
})
}
databaseCases := []struct {
name string
mutate func(*databasev1alpha1.PostgreSQLDatabase)
}{
{"missing-instance", func(d *databasev1alpha1.PostgreSQLDatabase) { d.Spec.InstanceRef.Name = "" }},
{"missing-role", func(d *databasev1alpha1.PostgreSQLDatabase) { d.Spec.LoginRole = "" }},
{"unknown-source", func(d *databasev1alpha1.PostgreSQLDatabase) { d.Spec.Source = "Adopt" }},
{"missing-credentials", func(d *databasev1alpha1.PostgreSQLDatabase) { d.Spec.Source = "Import" }},
{"provision-credentials", func(d *databasev1alpha1.PostgreSQLDatabase) {
d.Spec.CredentialRef = &databasev1alpha1.CredentialReference{Mount: "secret", Path: "existing"}
}},
{"unknown-policy", func(d *databasev1alpha1.PostgreSQLDatabase) { d.Spec.ReclaimPolicy = "Recycle" }},
{"binding-without-uid", func(d *databasev1alpha1.PostgreSQLDatabase) {
d.Spec.TenantRef = &databasev1alpha1.TenantReference{Namespace: testNamespace, Name: "tenant"}
}},
}
for _, tc := range databaseCases {
t.Run(tc.name, func(t *testing.T) {
object := validDatabase(tc.name)
tc.mutate(object)
requireInvalidCreate(t, client, object)
})
}
t.Run("互斥申请入口", func(t *testing.T) {
tenant := validTenant("ambiguous")
tenant.Spec.DatabaseRef = &databasev1alpha1.DatabaseReference{Name: "existing"}
requireInvalidCreate(t, client, tenant)
tenant.Spec.Provision = nil
tenant.Spec.DatabaseRef = nil
requireInvalidCreate(t, client, tenant)
})
}
// 本测试验证 API 写入语义,不模拟或宣称已经实现 controller 的恢复循环。
func testBindingWrites(t *testing.T, client ctrlclient.Client) {
ctx := t.Context()
tenant := validTenant("binding")
tenant.Status.Phase = readyPhase
if err := client.Create(ctx, tenant); err != nil {
t.Fatal(err)
}
if tenant.Status.Phase != "" {
t.Fatal("普通 Create 不应写入 status")
}
database := validDatabase("binding")
if err := client.Create(ctx, database); err != nil {
t.Fatal(err)
}
stale := database.DeepCopy()
database.Spec.TenantRef = &databasev1alpha1.TenantReference{
Namespace: tenant.Namespace, Name: databasev1alpha1.ObjectName(tenant.Name), UID: tenant.UID,
}
if err := client.Update(ctx, database); err != nil {
t.Fatal(err)
}
stale.Spec.TenantRef = &databasev1alpha1.TenantReference{Namespace: tenant.Namespace, Name: "other", UID: "other-uid"}
if err := client.Update(ctx, stale); !apierrors.IsConflict(err) {
t.Fatalf("过期并发写入 = %v, want Conflict", err)
}
// 换用 API 回读的对象补第二步,证明恢复所需记录不依赖先前内存。
observedDatabase := &databasev1alpha1.PostgreSQLDatabase{}
if err := client.Get(ctx, ctrlclient.ObjectKeyFromObject(database), observedDatabase); err != nil {
t.Fatal(err)
}
if observedDatabase.Spec.TenantRef.UID != tenant.UID {
t.Fatal("资源侧绑定被竞争写入覆盖")
}
beforeGeneration := tenant.Generation
tenant.Status.DatabaseRef = &databasev1alpha1.BoundDatabaseReference{
Name: databasev1alpha1.ObjectName(database.Name), UID: database.UID,
}
if err := client.Status().Update(ctx, tenant); err != nil {
t.Fatal(err)
}
if tenant.Generation != beforeGeneration || tenant.Status.DatabaseRef.UID != database.UID {
t.Fatal("status 更新错误地影响 generation 或绑定身份")
}
tenant.Status.Phase = readyPhase
if err := client.Update(ctx, tenant); err != nil {
t.Fatal(err)
}
if tenant.Status.Phase != "" {
t.Fatal("普通 Update 不应修改 status")
}
condition := metav1.Condition{Type: readyPhase, Status: metav1.ConditionFalse,
Reason: "Pending", Message: "尚未验证后端", LastTransitionTime: metav1.Now()}
tenant.Status.Conditions = []metav1.Condition{condition, condition}
if err := client.Status().Update(ctx, tenant); !apierrors.IsInvalid(err) {
t.Fatalf("重复 Condition = %v, want Invalid", err)
}
}
func requireInvalidCreate(t *testing.T, client ctrlclient.Client, object ctrlclient.Object) {
t.Helper()
if err := client.Create(context.Background(), object); !apierrors.IsInvalid(err) {
t.Fatalf("Create %T = %v, want Invalid", object, err)
}
}
func validInstance(name string) *databasev1alpha1.PostgreSQLInstance {
object := &databasev1alpha1.PostgreSQLInstance{}
object.Name = name
object.Spec.Endpoint = databasev1alpha1.PostgreSQLEndpoint{Host: "postgres.example.test", HostAddr: "127.0.0.1"}
object.Spec.AdminCredentialRef.Name = "postgres-admin"
return object
}
func validDatabase(name string) *databasev1alpha1.PostgreSQLDatabase {
object := &databasev1alpha1.PostgreSQLDatabase{}
object.Name = name
object.Spec = databasev1alpha1.PostgreSQLDatabaseSpec{
InstanceRef: databasev1alpha1.InstanceReference{Name: testInstanceName},
Database: "app", LoginRole: "app", Source: "Provision",
}
return object
}
func validTenant(name string) *databasev1alpha1.PostgreSQLTenant {
object := &databasev1alpha1.PostgreSQLTenant{}
object.Name = name
object.Namespace = testNamespace
object.Spec.Provision = &databasev1alpha1.DatabaseProvisionRequest{
InstanceRef: databasev1alpha1.InstanceReference{Name: testInstanceName},
}
return object
}
@@ -0,0 +1,457 @@
//go:build !ignore_autogenerated
// Code generated by controller-gen. DO NOT EDIT.
package v1alpha1
import (
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AdminCredentialReference) DeepCopyInto(out *AdminCredentialReference) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdminCredentialReference.
func (in *AdminCredentialReference) DeepCopy() *AdminCredentialReference {
if in == nil {
return nil
}
out := new(AdminCredentialReference)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BoundDatabaseReference) DeepCopyInto(out *BoundDatabaseReference) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BoundDatabaseReference.
func (in *BoundDatabaseReference) DeepCopy() *BoundDatabaseReference {
if in == nil {
return nil
}
out := new(BoundDatabaseReference)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CredentialReference) DeepCopyInto(out *CredentialReference) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CredentialReference.
func (in *CredentialReference) DeepCopy() *CredentialReference {
if in == nil {
return nil
}
out := new(CredentialReference)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DatabaseProvisionRequest) DeepCopyInto(out *DatabaseProvisionRequest) {
*out = *in
out.InstanceRef = in.InstanceRef
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseProvisionRequest.
func (in *DatabaseProvisionRequest) DeepCopy() *DatabaseProvisionRequest {
if in == nil {
return nil
}
out := new(DatabaseProvisionRequest)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DatabaseReference) DeepCopyInto(out *DatabaseReference) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseReference.
func (in *DatabaseReference) DeepCopy() *DatabaseReference {
if in == nil {
return nil
}
out := new(DatabaseReference)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *InstanceReference) DeepCopyInto(out *InstanceReference) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstanceReference.
func (in *InstanceReference) DeepCopy() *InstanceReference {
if in == nil {
return nil
}
out := new(InstanceReference)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PostgreSQLDatabase) DeepCopyInto(out *PostgreSQLDatabase) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLDatabase.
func (in *PostgreSQLDatabase) DeepCopy() *PostgreSQLDatabase {
if in == nil {
return nil
}
out := new(PostgreSQLDatabase)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *PostgreSQLDatabase) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PostgreSQLDatabaseList) DeepCopyInto(out *PostgreSQLDatabaseList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]PostgreSQLDatabase, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLDatabaseList.
func (in *PostgreSQLDatabaseList) DeepCopy() *PostgreSQLDatabaseList {
if in == nil {
return nil
}
out := new(PostgreSQLDatabaseList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *PostgreSQLDatabaseList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PostgreSQLDatabaseSpec) DeepCopyInto(out *PostgreSQLDatabaseSpec) {
*out = *in
out.InstanceRef = in.InstanceRef
if in.CredentialRef != nil {
in, out := &in.CredentialRef, &out.CredentialRef
*out = new(CredentialReference)
**out = **in
}
if in.TenantRef != nil {
in, out := &in.TenantRef, &out.TenantRef
*out = new(TenantReference)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLDatabaseSpec.
func (in *PostgreSQLDatabaseSpec) DeepCopy() *PostgreSQLDatabaseSpec {
if in == nil {
return nil
}
out := new(PostgreSQLDatabaseSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PostgreSQLDatabaseStatus) DeepCopyInto(out *PostgreSQLDatabaseStatus) {
*out = *in
if in.CredentialRef != nil {
in, out := &in.CredentialRef, &out.CredentialRef
*out = new(CredentialReference)
**out = **in
}
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]v1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLDatabaseStatus.
func (in *PostgreSQLDatabaseStatus) DeepCopy() *PostgreSQLDatabaseStatus {
if in == nil {
return nil
}
out := new(PostgreSQLDatabaseStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PostgreSQLEndpoint) DeepCopyInto(out *PostgreSQLEndpoint) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLEndpoint.
func (in *PostgreSQLEndpoint) DeepCopy() *PostgreSQLEndpoint {
if in == nil {
return nil
}
out := new(PostgreSQLEndpoint)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PostgreSQLInstance) DeepCopyInto(out *PostgreSQLInstance) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Spec = in.Spec
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLInstance.
func (in *PostgreSQLInstance) DeepCopy() *PostgreSQLInstance {
if in == nil {
return nil
}
out := new(PostgreSQLInstance)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *PostgreSQLInstance) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PostgreSQLInstanceList) DeepCopyInto(out *PostgreSQLInstanceList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]PostgreSQLInstance, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLInstanceList.
func (in *PostgreSQLInstanceList) DeepCopy() *PostgreSQLInstanceList {
if in == nil {
return nil
}
out := new(PostgreSQLInstanceList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *PostgreSQLInstanceList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PostgreSQLInstanceSpec) DeepCopyInto(out *PostgreSQLInstanceSpec) {
*out = *in
out.Endpoint = in.Endpoint
out.AdminCredentialRef = in.AdminCredentialRef
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLInstanceSpec.
func (in *PostgreSQLInstanceSpec) DeepCopy() *PostgreSQLInstanceSpec {
if in == nil {
return nil
}
out := new(PostgreSQLInstanceSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PostgreSQLInstanceStatus) DeepCopyInto(out *PostgreSQLInstanceStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]v1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLInstanceStatus.
func (in *PostgreSQLInstanceStatus) DeepCopy() *PostgreSQLInstanceStatus {
if in == nil {
return nil
}
out := new(PostgreSQLInstanceStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PostgreSQLTenant) DeepCopyInto(out *PostgreSQLTenant) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLTenant.
func (in *PostgreSQLTenant) DeepCopy() *PostgreSQLTenant {
if in == nil {
return nil
}
out := new(PostgreSQLTenant)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *PostgreSQLTenant) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PostgreSQLTenantList) DeepCopyInto(out *PostgreSQLTenantList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]PostgreSQLTenant, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLTenantList.
func (in *PostgreSQLTenantList) DeepCopy() *PostgreSQLTenantList {
if in == nil {
return nil
}
out := new(PostgreSQLTenantList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *PostgreSQLTenantList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PostgreSQLTenantSpec) DeepCopyInto(out *PostgreSQLTenantSpec) {
*out = *in
if in.Provision != nil {
in, out := &in.Provision, &out.Provision
*out = new(DatabaseProvisionRequest)
**out = **in
}
if in.DatabaseRef != nil {
in, out := &in.DatabaseRef, &out.DatabaseRef
*out = new(DatabaseReference)
**out = **in
}
if in.Extensions != nil {
in, out := &in.Extensions, &out.Extensions
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLTenantSpec.
func (in *PostgreSQLTenantSpec) DeepCopy() *PostgreSQLTenantSpec {
if in == nil {
return nil
}
out := new(PostgreSQLTenantSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PostgreSQLTenantStatus) DeepCopyInto(out *PostgreSQLTenantStatus) {
*out = *in
if in.DatabaseRef != nil {
in, out := &in.DatabaseRef, &out.DatabaseRef
*out = new(BoundDatabaseReference)
**out = **in
}
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]v1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLTenantStatus.
func (in *PostgreSQLTenantStatus) DeepCopy() *PostgreSQLTenantStatus {
if in == nil {
return nil
}
out := new(PostgreSQLTenantStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantReference) DeepCopyInto(out *TenantReference) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantReference.
func (in *TenantReference) DeepCopy() *TenantReference {
if in == nil {
return nil
}
out := new(TenantReference)
in.DeepCopyInto(out)
return out
}
@@ -0,0 +1,28 @@
// Package v1alpha1 contains API Schema definitions for the execution v1alpha1 API group.
// +kubebuilder:object:generate=true
// +groupName=execution.ayatori.ddupan.top
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
var (
// SchemeGroupVersion is group version used to register these objects.
// This name is used by applyconfiguration generators (e.g. controller-gen).
SchemeGroupVersion = schema.GroupVersion{Group: "execution.ayatori.ddupan.top", Version: "v1alpha1"}
// GroupVersion is an alias for SchemeGroupVersion, for backward compatibility.
GroupVersion = SchemeGroupVersion
// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
SchemeBuilder = runtime.NewSchemeBuilder(func(scheme *runtime.Scheme) error {
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
})
// AddToScheme adds the types in this group-version to the given scheme.
AddToScheme = SchemeBuilder.AddToScheme
)
+180
View File
@@ -0,0 +1,180 @@
package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
)
const (
JobConditionAccepted = "Accepted"
JobConditionScheduled = "Scheduled"
JobConditionSucceeded = "Succeeded"
)
type JobDesiredState string
const (
JobDesiredStateRunning JobDesiredState = "Running"
JobDesiredStateCancelled JobDesiredState = "Cancelled"
)
type TaskSpec struct {
// +kubebuilder:validation:MinLength=1
Image string `json:"image"`
// +optional
ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"`
// +optional
Command []string `json:"command,omitempty"`
// +optional
Args []string `json:"args,omitempty"`
// +optional
WorkingDir string `json:"workingDir,omitempty"`
// +listType=map
// +listMapKey=name
// +optional
Env []EnvVar `json:"env,omitempty"`
}
// +kubebuilder:validation:XValidation:rule="has(self.value) != has(self.valueFrom)",message="exactly one of value or valueFrom must be set"
type EnvVar struct {
// +kubebuilder:validation:Pattern=`^[A-Za-z_][A-Za-z0-9_]*$`
Name string `json:"name"`
// +optional
Value *string `json:"value,omitempty"`
// +optional
ValueFrom *EnvVarSource `json:"valueFrom,omitempty"`
}
// +kubebuilder:validation:XValidation:rule="has(self.secretKeyRef) != has(self.configMapKeyRef)",message="exactly one key reference must be set"
type EnvVarSource struct {
// +optional
SecretKeyRef *corev1.SecretKeySelector `json:"secretKeyRef,omitempty"`
// +optional
ConfigMapKeyRef *corev1.ConfigMapKeySelector `json:"configMapKeyRef,omitempty"`
}
type ResourceValues struct {
// +optional
CPU *resource.Quantity `json:"cpu,omitempty"`
// +optional
Memory *resource.Quantity `json:"memory,omitempty"`
}
type ExecutionResourceRequirements struct {
// +optional
Requests ResourceValues `json:"requests,omitempty"`
// +optional
Limits ResourceValues `json:"limits,omitempty"`
}
// +kubebuilder:validation:XValidation:rule="self.task == oldSelf.task",message="task is immutable"
// +kubebuilder:validation:XValidation:rule="self.resources == oldSelf.resources",message="resources are immutable"
// +kubebuilder:validation:XValidation:rule="has(self.activeDeadlineSeconds) == has(oldSelf.activeDeadlineSeconds) && (!has(self.activeDeadlineSeconds) || self.activeDeadlineSeconds == oldSelf.activeDeadlineSeconds)",message="activeDeadlineSeconds is immutable"
// +kubebuilder:validation:XValidation:rule="has(self.jobClassName) == has(oldSelf.jobClassName) && (!has(self.jobClassName) || self.jobClassName == oldSelf.jobClassName)",message="jobClassName is immutable"
// +kubebuilder:validation:XValidation:rule="oldSelf.desiredState == self.desiredState || (oldSelf.desiredState == 'Running' && self.desiredState == 'Cancelled')",message="desiredState may only transition from Running to Cancelled"
type JobSpec struct {
// +optional
JobClassName string `json:"jobClassName,omitempty"`
Task TaskSpec `json:"task"`
// +optional
Resources ExecutionResourceRequirements `json:"resources,omitempty"`
// +kubebuilder:validation:Minimum=1
// +optional
ActiveDeadlineSeconds *int64 `json:"activeDeadlineSeconds,omitempty"`
// +kubebuilder:validation:Minimum=0
// +optional
TTLSecondsAfterFinished *int32 `json:"ttlSecondsAfterFinished,omitempty"`
// +kubebuilder:validation:Enum=Running;Cancelled
// +kubebuilder:default=Running
// +optional
DesiredState JobDesiredState `json:"desiredState,omitempty"`
}
type ParametersReference struct {
Group string `json:"group"`
Kind string `json:"kind"`
Name string `json:"name"`
// +optional
UID types.UID `json:"uid,omitempty"`
}
type ResolvedJobClassReference struct {
Name string `json:"name"`
UID types.UID `json:"uid"`
ControllerName string `json:"controllerName"`
ParametersRef ParametersReference `json:"parametersRef"`
}
type ExecutionReference struct {
// +kubebuilder:validation:MinLength=1
Type string `json:"type"`
// +kubebuilder:validation:MinLength=1
ID string `json:"id"`
}
type ExecutionStatus struct {
Adapter string `json:"adapter"`
// +listType=map
// +listMapKey=type
// +optional
References []ExecutionReference `json:"references,omitempty"`
}
type JobResult struct {
// +optional
ExitCode *int32 `json:"exitCode,omitempty"`
// +optional
Reason string `json:"reason,omitempty"`
}
type JobStatus struct {
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// +listType=map
// +listMapKey=type
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
// +optional
ResolvedJobClass *ResolvedJobClassReference `json:"resolvedJobClass,omitempty"`
// +optional
EffectiveResources ExecutionResourceRequirements `json:"effectiveResources,omitempty"`
// +optional
Execution *ExecutionStatus `json:"execution,omitempty"`
// +optional
StartTime *metav1.Time `json:"startTime,omitempty"`
// +optional
CompletionTime *metav1.Time `json:"completionTime,omitempty"`
// +optional
Result *JobResult `json:"result,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Accepted",type=string,JSONPath=`.status.conditions[?(@.type=='Accepted')].status`
// +kubebuilder:printcolumn:name="Scheduled",type=string,JSONPath=`.status.conditions[?(@.type=='Scheduled')].status`
// +kubebuilder:printcolumn:name="Succeeded",type=string,JSONPath=`.status.conditions[?(@.type=='Succeeded')].status`
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
type Job struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitzero"`
Spec JobSpec `json:"spec"`
// +optional
Status JobStatus `json:"status,omitzero"`
}
// +kubebuilder:object:root=true
type JobList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitzero"`
Items []Job `json:"items"`
}
func init() {
SchemeBuilder.Register(func(s *runtime.Scheme) error {
s.AddKnownTypes(SchemeGroupVersion, &Job{}, &JobList{})
return nil
})
}
+71
View File
@@ -0,0 +1,71 @@
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
const (
JobClassConditionAccepted = "Accepted"
JobClassConditionReady = "Ready"
)
type ExecutionResourcePolicy struct {
// +optional
Defaults ExecutionResourceRequirements `json:"defaults,omitempty"`
// +optional
Minimum ExecutionResourceRequirements `json:"minimum,omitempty"`
// +optional
Maximum ExecutionResourceRequirements `json:"maximum,omitempty"`
}
// +kubebuilder:validation:XValidation:rule="self.controllerName == oldSelf.controllerName",message="controllerName is immutable"
// +kubebuilder:validation:XValidation:rule="self.parametersRef == oldSelf.parametersRef",message="parametersRef is immutable"
type JobClassSpec struct {
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
ControllerName string `json:"controllerName"`
ParametersRef ParametersReference `json:"parametersRef"`
// +optional
AllowedNamespaces *metav1.LabelSelector `json:"allowedNamespaces,omitempty"`
// +optional
Resources ExecutionResourcePolicy `json:"resources,omitempty"`
}
type JobClassStatus struct {
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// +listType=map
// +listMapKey=type
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster
// +kubebuilder:printcolumn:name="Controller",type=string,JSONPath=`.spec.controllerName`
// +kubebuilder:printcolumn:name="Accepted",type=string,JSONPath=`.status.conditions[?(@.type=='Accepted')].status`
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=='Ready')].status`
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
type JobClass struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitzero"`
Spec JobClassSpec `json:"spec"`
// +optional
Status JobClassStatus `json:"status,omitzero"`
}
// +kubebuilder:object:root=true
type JobClassList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitzero"`
Items []JobClass `json:"items"`
}
func init() {
SchemeBuilder.Register(func(s *runtime.Scheme) error {
s.AddKnownTypes(SchemeGroupVersion, &JobClass{}, &JobClassList{})
return nil
})
}
@@ -0,0 +1,51 @@
package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
type KubernetesSchedulingParameters struct {
// +optional
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
// +optional
Tolerations []corev1.Toleration `json:"tolerations,omitempty"`
}
type KubernetesExecutionParametersSpec struct {
// +kubebuilder:validation:MinLength=1
ServiceAccountName string `json:"serviceAccountName"`
// +optional
RuntimeClassName string `json:"runtimeClassName,omitempty"`
// +optional
Scheduling KubernetesSchedulingParameters `json:"scheduling,omitempty"`
// +optional
PodSecurityContext *corev1.PodSecurityContext `json:"podSecurityContext,omitempty"`
// +kubebuilder:validation:Enum=Always;Never;IfNotPresent
// +kubebuilder:default=IfNotPresent
// +optional
ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:resource:scope=Cluster
type KubernetesExecutionParameters struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitzero"`
Spec KubernetesExecutionParametersSpec `json:"spec"`
}
// +kubebuilder:object:root=true
type KubernetesExecutionParametersList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitzero"`
Items []KubernetesExecutionParameters `json:"items"`
}
func init() {
SchemeBuilder.Register(func(s *runtime.Scheme) error {
s.AddKnownTypes(SchemeGroupVersion, &KubernetesExecutionParameters{}, &KubernetesExecutionParametersList{})
return nil
})
}
@@ -0,0 +1,61 @@
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
type NamespacedKeyReference struct {
Namespace string `json:"namespace"`
Name string `json:"name"`
Key string `json:"key"`
}
type OpenSandboxRequestMapping string
const (
OpenSandboxRequestMappingAdmissionOnly OpenSandboxRequestMapping = "AdmissionOnly"
OpenSandboxRequestMappingNative OpenSandboxRequestMapping = "Native"
)
// +kubebuilder:validation:XValidation:rule="self.allowInsecureHTTP || self.endpoint.startsWith('https://')",message="endpoint must use HTTPS unless allowInsecureHTTP is true"
type OpenSandboxExecutionParametersSpec struct {
// +kubebuilder:validation:MinLength=1
Endpoint string `json:"endpoint"`
APIKeySecretRef NamespacedKeyReference `json:"apiKeySecretRef"`
// +optional
PoolRef string `json:"poolRef,omitempty"`
// +kubebuilder:validation:Enum=AdmissionOnly;Native
// +kubebuilder:default=AdmissionOnly
// +optional
RequestMapping OpenSandboxRequestMapping `json:"requestMapping,omitempty"`
// +optional
AllowSecretEnv bool `json:"allowSecretEnv,omitempty"`
// +optional
AllowImageAuth bool `json:"allowImageAuth,omitempty"`
// AllowInsecureHTTP is intended for isolated development environments only.
// +optional
AllowInsecureHTTP bool `json:"allowInsecureHTTP,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:resource:scope=Cluster
type OpenSandboxExecutionParameters struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitzero"`
Spec OpenSandboxExecutionParametersSpec `json:"spec"`
}
// +kubebuilder:object:root=true
type OpenSandboxExecutionParametersList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitzero"`
Items []OpenSandboxExecutionParameters `json:"items"`
}
func init() {
SchemeBuilder.Register(func(s *runtime.Scheme) error {
s.AddKnownTypes(SchemeGroupVersion, &OpenSandboxExecutionParameters{}, &OpenSandboxExecutionParametersList{})
return nil
})
}
+118
View File
@@ -0,0 +1,118 @@
package v1alpha1_test
import (
"context"
"os"
"path/filepath"
"testing"
executionv1alpha1 "git.ddupan.top/panxiao81/ayatori/api/execution/v1alpha1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
)
func TestJobCRDValidation(t *testing.T) {
if os.Getenv("KUBEBUILDER_ASSETS") == "" {
t.Skip("KUBEBUILDER_ASSETS is unset; run make test to execute API integration tests")
}
scheme := runtime.NewScheme()
if err := executionv1alpha1.AddToScheme(scheme); err != nil {
t.Fatal(err)
}
if err := corev1.AddToScheme(scheme); err != nil {
t.Fatal(err)
}
crdPath, err := filepath.Abs("../../../config/crd/bases")
if err != nil {
t.Fatal(err)
}
environment := &envtest.Environment{CRDDirectoryPaths: []string{crdPath}}
config, err := environment.Start()
if err != nil {
t.Fatalf("start envtest: %v", err)
}
t.Cleanup(func() {
if err := environment.Stop(); err != nil {
t.Errorf("stop envtest: %v", err)
}
})
client, err := ctrlclient.New(config, ctrlclient.Options{Scheme: scheme})
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
//nolint:modernize // ObjectMeta is promoted through embedded TypeMeta; embedlit produces invalid Go here.
namespace := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "api-validation"}}
if err := client.Create(ctx, namespace); err != nil {
t.Fatal(err)
}
t.Run("defaults desired state", func(t *testing.T) {
job := validJob("defaults")
if err := client.Create(ctx, job); err != nil {
t.Fatal(err)
}
if job.Spec.DesiredState != executionv1alpha1.JobDesiredStateRunning {
t.Fatalf("desiredState = %q, want Running", job.Spec.DesiredState)
}
})
t.Run("rejects ambiguous environment value", func(t *testing.T) {
literal := "visible"
job := validJob("invalid-env")
job.Spec.Task.Env = []executionv1alpha1.EnvVar{{
Name: "TOKEN",
Value: &literal,
ValueFrom: &executionv1alpha1.EnvVarSource{
//nolint:modernize // LocalObjectReference is an embedded Kubernetes API field.
SecretKeyRef: &corev1.SecretKeySelector{LocalObjectReference: corev1.LocalObjectReference{Name: "token"}, Key: "value"},
},
}}
if err := client.Create(ctx, job); !apierrors.IsInvalid(err) {
t.Fatalf("Create() error = %v, want Invalid", err)
}
})
t.Run("rejects immutable task update", func(t *testing.T) {
job := validJob("immutable")
if err := client.Create(ctx, job); err != nil {
t.Fatal(err)
}
job.Spec.Task.Image = "docker.io/library/busybox:1.37"
if err := client.Update(ctx, job); !apierrors.IsInvalid(err) {
t.Fatalf("Update() error = %v, want Invalid", err)
}
})
t.Run("allows one-way cancellation", func(t *testing.T) {
job := validJob("cancel")
if err := client.Create(ctx, job); err != nil {
t.Fatal(err)
}
job.Spec.DesiredState = executionv1alpha1.JobDesiredStateCancelled
if err := client.Update(ctx, job); err != nil {
t.Fatalf("cancel update: %v", err)
}
job.Spec.DesiredState = executionv1alpha1.JobDesiredStateRunning
if err := client.Update(ctx, job); !apierrors.IsInvalid(err) {
t.Fatalf("reverse cancellation error = %v, want Invalid", err)
}
})
}
func validJob(name string) *executionv1alpha1.Job {
//nolint:modernize // ObjectMeta is promoted through embedded TypeMeta; embedlit produces invalid Go here.
return &executionv1alpha1.Job{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "api-validation"},
Spec: executionv1alpha1.JobSpec{
Task: executionv1alpha1.TaskSpec{Image: "docker.io/library/alpine:3.22"},
},
}
}
@@ -0,0 +1,676 @@
//go:build !ignore_autogenerated
// Code generated by controller-gen. DO NOT EDIT.
package v1alpha1
import (
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *EnvVar) DeepCopyInto(out *EnvVar) {
*out = *in
if in.Value != nil {
in, out := &in.Value, &out.Value
*out = new(string)
**out = **in
}
if in.ValueFrom != nil {
in, out := &in.ValueFrom, &out.ValueFrom
*out = new(EnvVarSource)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvVar.
func (in *EnvVar) DeepCopy() *EnvVar {
if in == nil {
return nil
}
out := new(EnvVar)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *EnvVarSource) DeepCopyInto(out *EnvVarSource) {
*out = *in
if in.SecretKeyRef != nil {
in, out := &in.SecretKeyRef, &out.SecretKeyRef
*out = new(v1.SecretKeySelector)
(*in).DeepCopyInto(*out)
}
if in.ConfigMapKeyRef != nil {
in, out := &in.ConfigMapKeyRef, &out.ConfigMapKeyRef
*out = new(v1.ConfigMapKeySelector)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvVarSource.
func (in *EnvVarSource) DeepCopy() *EnvVarSource {
if in == nil {
return nil
}
out := new(EnvVarSource)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ExecutionReference) DeepCopyInto(out *ExecutionReference) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExecutionReference.
func (in *ExecutionReference) DeepCopy() *ExecutionReference {
if in == nil {
return nil
}
out := new(ExecutionReference)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ExecutionResourcePolicy) DeepCopyInto(out *ExecutionResourcePolicy) {
*out = *in
in.Defaults.DeepCopyInto(&out.Defaults)
in.Minimum.DeepCopyInto(&out.Minimum)
in.Maximum.DeepCopyInto(&out.Maximum)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExecutionResourcePolicy.
func (in *ExecutionResourcePolicy) DeepCopy() *ExecutionResourcePolicy {
if in == nil {
return nil
}
out := new(ExecutionResourcePolicy)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ExecutionResourceRequirements) DeepCopyInto(out *ExecutionResourceRequirements) {
*out = *in
in.Requests.DeepCopyInto(&out.Requests)
in.Limits.DeepCopyInto(&out.Limits)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExecutionResourceRequirements.
func (in *ExecutionResourceRequirements) DeepCopy() *ExecutionResourceRequirements {
if in == nil {
return nil
}
out := new(ExecutionResourceRequirements)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ExecutionStatus) DeepCopyInto(out *ExecutionStatus) {
*out = *in
if in.References != nil {
in, out := &in.References, &out.References
*out = make([]ExecutionReference, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExecutionStatus.
func (in *ExecutionStatus) DeepCopy() *ExecutionStatus {
if in == nil {
return nil
}
out := new(ExecutionStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Job) DeepCopyInto(out *Job) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Job.
func (in *Job) DeepCopy() *Job {
if in == nil {
return nil
}
out := new(Job)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Job) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *JobClass) DeepCopyInto(out *JobClass) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobClass.
func (in *JobClass) DeepCopy() *JobClass {
if in == nil {
return nil
}
out := new(JobClass)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *JobClass) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *JobClassList) DeepCopyInto(out *JobClassList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]JobClass, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobClassList.
func (in *JobClassList) DeepCopy() *JobClassList {
if in == nil {
return nil
}
out := new(JobClassList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *JobClassList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *JobClassSpec) DeepCopyInto(out *JobClassSpec) {
*out = *in
out.ParametersRef = in.ParametersRef
if in.AllowedNamespaces != nil {
in, out := &in.AllowedNamespaces, &out.AllowedNamespaces
*out = new(metav1.LabelSelector)
(*in).DeepCopyInto(*out)
}
in.Resources.DeepCopyInto(&out.Resources)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobClassSpec.
func (in *JobClassSpec) DeepCopy() *JobClassSpec {
if in == nil {
return nil
}
out := new(JobClassSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *JobClassStatus) DeepCopyInto(out *JobClassStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]metav1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobClassStatus.
func (in *JobClassStatus) DeepCopy() *JobClassStatus {
if in == nil {
return nil
}
out := new(JobClassStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *JobList) DeepCopyInto(out *JobList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Job, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobList.
func (in *JobList) DeepCopy() *JobList {
if in == nil {
return nil
}
out := new(JobList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *JobList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *JobResult) DeepCopyInto(out *JobResult) {
*out = *in
if in.ExitCode != nil {
in, out := &in.ExitCode, &out.ExitCode
*out = new(int32)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobResult.
func (in *JobResult) DeepCopy() *JobResult {
if in == nil {
return nil
}
out := new(JobResult)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *JobSpec) DeepCopyInto(out *JobSpec) {
*out = *in
in.Task.DeepCopyInto(&out.Task)
in.Resources.DeepCopyInto(&out.Resources)
if in.ActiveDeadlineSeconds != nil {
in, out := &in.ActiveDeadlineSeconds, &out.ActiveDeadlineSeconds
*out = new(int64)
**out = **in
}
if in.TTLSecondsAfterFinished != nil {
in, out := &in.TTLSecondsAfterFinished, &out.TTLSecondsAfterFinished
*out = new(int32)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobSpec.
func (in *JobSpec) DeepCopy() *JobSpec {
if in == nil {
return nil
}
out := new(JobSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *JobStatus) DeepCopyInto(out *JobStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]metav1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.ResolvedJobClass != nil {
in, out := &in.ResolvedJobClass, &out.ResolvedJobClass
*out = new(ResolvedJobClassReference)
**out = **in
}
in.EffectiveResources.DeepCopyInto(&out.EffectiveResources)
if in.Execution != nil {
in, out := &in.Execution, &out.Execution
*out = new(ExecutionStatus)
(*in).DeepCopyInto(*out)
}
if in.StartTime != nil {
in, out := &in.StartTime, &out.StartTime
*out = (*in).DeepCopy()
}
if in.CompletionTime != nil {
in, out := &in.CompletionTime, &out.CompletionTime
*out = (*in).DeepCopy()
}
if in.Result != nil {
in, out := &in.Result, &out.Result
*out = new(JobResult)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobStatus.
func (in *JobStatus) DeepCopy() *JobStatus {
if in == nil {
return nil
}
out := new(JobStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubernetesExecutionParameters) DeepCopyInto(out *KubernetesExecutionParameters) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesExecutionParameters.
func (in *KubernetesExecutionParameters) DeepCopy() *KubernetesExecutionParameters {
if in == nil {
return nil
}
out := new(KubernetesExecutionParameters)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *KubernetesExecutionParameters) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubernetesExecutionParametersList) DeepCopyInto(out *KubernetesExecutionParametersList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]KubernetesExecutionParameters, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesExecutionParametersList.
func (in *KubernetesExecutionParametersList) DeepCopy() *KubernetesExecutionParametersList {
if in == nil {
return nil
}
out := new(KubernetesExecutionParametersList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *KubernetesExecutionParametersList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubernetesExecutionParametersSpec) DeepCopyInto(out *KubernetesExecutionParametersSpec) {
*out = *in
in.Scheduling.DeepCopyInto(&out.Scheduling)
if in.PodSecurityContext != nil {
in, out := &in.PodSecurityContext, &out.PodSecurityContext
*out = new(v1.PodSecurityContext)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesExecutionParametersSpec.
func (in *KubernetesExecutionParametersSpec) DeepCopy() *KubernetesExecutionParametersSpec {
if in == nil {
return nil
}
out := new(KubernetesExecutionParametersSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubernetesSchedulingParameters) DeepCopyInto(out *KubernetesSchedulingParameters) {
*out = *in
if in.NodeSelector != nil {
in, out := &in.NodeSelector, &out.NodeSelector
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.Tolerations != nil {
in, out := &in.Tolerations, &out.Tolerations
*out = make([]v1.Toleration, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesSchedulingParameters.
func (in *KubernetesSchedulingParameters) DeepCopy() *KubernetesSchedulingParameters {
if in == nil {
return nil
}
out := new(KubernetesSchedulingParameters)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NamespacedKeyReference) DeepCopyInto(out *NamespacedKeyReference) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespacedKeyReference.
func (in *NamespacedKeyReference) DeepCopy() *NamespacedKeyReference {
if in == nil {
return nil
}
out := new(NamespacedKeyReference)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OpenSandboxExecutionParameters) DeepCopyInto(out *OpenSandboxExecutionParameters) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Spec = in.Spec
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenSandboxExecutionParameters.
func (in *OpenSandboxExecutionParameters) DeepCopy() *OpenSandboxExecutionParameters {
if in == nil {
return nil
}
out := new(OpenSandboxExecutionParameters)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *OpenSandboxExecutionParameters) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OpenSandboxExecutionParametersList) DeepCopyInto(out *OpenSandboxExecutionParametersList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]OpenSandboxExecutionParameters, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenSandboxExecutionParametersList.
func (in *OpenSandboxExecutionParametersList) DeepCopy() *OpenSandboxExecutionParametersList {
if in == nil {
return nil
}
out := new(OpenSandboxExecutionParametersList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *OpenSandboxExecutionParametersList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OpenSandboxExecutionParametersSpec) DeepCopyInto(out *OpenSandboxExecutionParametersSpec) {
*out = *in
out.APIKeySecretRef = in.APIKeySecretRef
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenSandboxExecutionParametersSpec.
func (in *OpenSandboxExecutionParametersSpec) DeepCopy() *OpenSandboxExecutionParametersSpec {
if in == nil {
return nil
}
out := new(OpenSandboxExecutionParametersSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ParametersReference) DeepCopyInto(out *ParametersReference) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ParametersReference.
func (in *ParametersReference) DeepCopy() *ParametersReference {
if in == nil {
return nil
}
out := new(ParametersReference)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ResolvedJobClassReference) DeepCopyInto(out *ResolvedJobClassReference) {
*out = *in
out.ParametersRef = in.ParametersRef
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResolvedJobClassReference.
func (in *ResolvedJobClassReference) DeepCopy() *ResolvedJobClassReference {
if in == nil {
return nil
}
out := new(ResolvedJobClassReference)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ResourceValues) DeepCopyInto(out *ResourceValues) {
*out = *in
if in.CPU != nil {
in, out := &in.CPU, &out.CPU
x := (*in).DeepCopy()
*out = &x
}
if in.Memory != nil {
in, out := &in.Memory, &out.Memory
x := (*in).DeepCopy()
*out = &x
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceValues.
func (in *ResourceValues) DeepCopy() *ResourceValues {
if in == nil {
return nil
}
out := new(ResourceValues)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TaskSpec) DeepCopyInto(out *TaskSpec) {
*out = *in
if in.ImagePullSecrets != nil {
in, out := &in.ImagePullSecrets, &out.ImagePullSecrets
*out = make([]v1.LocalObjectReference, len(*in))
copy(*out, *in)
}
if in.Command != nil {
in, out := &in.Command, &out.Command
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Args != nil {
in, out := &in.Args, &out.Args
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Env != nil {
in, out := &in.Env, &out.Env
*out = make([]EnvVar, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TaskSpec.
func (in *TaskSpec) DeepCopy() *TaskSpec {
if in == nil {
return nil
}
out := new(TaskSpec)
in.DeepCopyInto(out)
return out
}
+15
View File
@@ -0,0 +1,15 @@
package main
import (
"os"
"git.ddupan.top/panxiao81/ayatori/internal/bootstrap"
ctrl "sigs.k8s.io/controller-runtime"
)
func main() {
if err := bootstrap.Run(); err != nil {
ctrl.Log.WithName("setup").Error(err, "Controller manager exited")
os.Exit(1)
}
}
@@ -0,0 +1,258 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.22.0
name: postgresqldatabases.database.ayatori.ddupan.top
spec:
group: database.ayatori.ddupan.top
names:
kind: PostgreSQLDatabase
listKind: PostgreSQLDatabaseList
plural: postgresqldatabases
singular: postgresqldatabase
scope: Cluster
versions:
- additionalPrinterColumns:
- jsonPath: .spec.instanceRef.name
name: Instance
type: string
- jsonPath: .spec.database
name: Database
type: string
- jsonPath: .status.conditions[?(@.type=='Ready')].status
name: Ready
type: string
name: v1alpha1
schema:
openAPIV3Schema:
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: PostgreSQLDatabaseSpec 是一库、一个 login owner 及凭据的独立资源声明。
properties:
credentialRef:
description: |-
CredentialReference 定位 OpenBao KV v2 凭据,不包含任何秘密值。
管理员在导入声明中指定,controller 在 status 中固定位置;两者均须检查部署允许的范围。
properties:
mount:
maxLength: 253
minLength: 1
type: string
path:
description: Path 是 mount 内的逻辑路径,不含 KV v2 的 data/ API 前缀。
maxLength: 1024
minLength: 1
type: string
required:
- mount
- path
type: object
database:
description: PostgreSQLIdentifier 是第一版受管 database 与 login role 使用的名称。
maxLength: 63
pattern: ^[a-z][a-z0-9_]{0,62}$
type: string
instanceRef:
description: InstanceReference 仅引用同 API group 的集群级 PostgreSQLInstance。
properties:
name:
description: ObjectName 定位集群级资源,不携带 namespace 或隐式跨 API group 引用。
maxLength: 253
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$
type: string
required:
- name
type: object
loginRole:
description: PostgreSQLIdentifier 是第一版受管 database 与 login role 使用的名称。
maxLength: 63
pattern: ^[a-z][a-z0-9_]{0,62}$
type: string
reclaimPolicy:
default: Retain
description: ReclaimPolicy 控制资源释放后的处置,只有资源管理者可以修改。
enum:
- Retain
- Delete
type: string
source:
description: Source 明确区分创建与只读导入,不从后端同名对象推断。
enum:
- Provision
- Import
type: string
tenantRef:
description: TenantRef 由 controller 先写入;Released 时仍保留旧身份。
properties:
name:
description: ObjectName 定位集群级资源,不携带 namespace 或隐式跨 API group 引用。
maxLength: 253
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$
type: string
namespace:
maxLength: 63
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
type: string
uid:
description: |-
UID is a type that holds unique ID values, including UUIDs. Because we
don't ONLY use UUIDs, this is an alias to string. Being a type captures
intent and helps make sure that UIDs and names do not get conflated.
maxLength: 128
minLength: 1
type: string
required:
- name
- namespace
- uid
type: object
required:
- database
- instanceRef
- loginRole
- source
type: object
x-kubernetes-validations:
- message: only imported databases require an existing credentialRef
rule: (self.source == 'Import') == has(self.credentialRef)
status:
properties:
conditions:
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
credentialRef:
description: CredentialRef 在首次外部写入前固定凭据位置;部署配置变化不迁移此位置。
properties:
mount:
maxLength: 253
minLength: 1
type: string
path:
description: Path 是 mount 内的逻辑路径,不含 KV v2 的 data/ API 前缀。
maxLength: 1024
minLength: 1
type: string
required:
- mount
- path
type: object
credentialVersion:
description: |-
CredentialVersion 只在创建并回读成功后记录,不表示凭据当前仍可用。
省略表示尚未确认;已有凭据不能仅凭读取成功补记确认。
format: int64
minimum: 1
type: integer
instanceUID:
description: InstanceUID 记录观察时的实例身份,不把同名新实例视为原目标。
type: string
observedGeneration:
format: int64
type: integer
phase:
description: Phase 暂不冻结供应子阶段枚举;它不是操作授权或绑定的替代记录。
type: string
type: object
required:
- spec
type: object
x-kubernetes-validations:
- message: credentialVersion requires credentialRef
rule: '!has(self.status) || !has(self.status.credentialVersion) || has(self.status.credentialRef)'
- message: recorded credentialRef cannot change or be removed
rule: '!(has(oldSelf.status) && has(oldSelf.status.credentialRef)) || (has(self.status)
&& has(self.status.credentialRef) && self.status.credentialRef == oldSelf.status.credentialRef)'
- message: confirmed credentialVersion cannot change or be removed
rule: '!(has(oldSelf.status) && has(oldSelf.status.credentialVersion)) ||
(has(self.status) && has(self.status.credentialVersion) && self.status.credentialVersion
== oldSelf.status.credentialVersion)'
- message: managed database target cannot change after observation or binding
starts
rule: '!(has(oldSelf.spec.tenantRef) || (has(oldSelf.status) && (has(oldSelf.status.instanceUID)
|| has(oldSelf.status.credentialRef)))) || (self.spec.instanceRef == oldSelf.spec.instanceRef
&& self.spec.database == oldSelf.spec.database && self.spec.loginRole
== oldSelf.spec.loginRole && self.spec.source == oldSelf.spec.source &&
has(self.spec.credentialRef) == has(oldSelf.spec.credentialRef) && (!has(oldSelf.spec.credentialRef)
|| self.spec.credentialRef == oldSelf.spec.credentialRef))'
served: true
storage: true
subresources:
status: {}
@@ -0,0 +1,191 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.22.0
name: postgresqlinstances.database.ayatori.ddupan.top
spec:
group: database.ayatori.ddupan.top
names:
kind: PostgreSQLInstance
listKind: PostgreSQLInstanceList
plural: postgresqlinstances
singular: postgresqlinstance
scope: Cluster
versions:
- additionalPrinterColumns:
- jsonPath: .status.conditions[?(@.type=='Ready')].status
name: Ready
type: string
name: v1alpha1
schema:
openAPIV3Schema:
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
properties:
adminCredentialRef:
description: AdminCredentialReference 只能读取 controller namespace 的
Secret。
properties:
name:
description: ObjectName 定位集群级资源,不携带 namespace 或隐式跨 API group 引用。
maxLength: 253
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$
type: string
passwordKey:
default: password
maxLength: 253
minLength: 1
pattern: ^[-._a-zA-Z0-9]+$
type: string
usernameKey:
default: username
maxLength: 253
minLength: 1
pattern: ^[-._a-zA-Z0-9]+$
type: string
required:
- name
type: object
endpoint:
description: PostgreSQLEndpoint 显式区分证书主机名与实际连接 IP,不进行 DNS 推导。
properties:
database:
default: postgres
description: PostgreSQLIdentifier 是第一版受管 database 与 login role
使用的名称。
maxLength: 63
pattern: ^[a-z][a-z0-9_]{0,62}$
type: string
host:
maxLength: 253
minLength: 1
type: string
hostaddr:
maxLength: 45
type: string
x-kubernetes-validations:
- message: hostaddr must be a single IPv4 or IPv6 address
rule: isIP(self)
port:
default: 5432
format: int32
maximum: 65535
minimum: 1
type: integer
sslMode:
default: verify-full
enum:
- disable
- require
- verify-ca
- verify-full
type: string
required:
- host
- hostaddr
type: object
required:
- adminCredentialRef
- endpoint
type: object
status:
properties:
conditions:
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
observedGeneration:
format: int64
type: integer
phase:
enum:
- Pending
- Validating
- Ready
- Deleting
type: string
postgresqlVersion:
type: string
type: object
required:
- spec
type: object
served: true
storage: true
subresources:
status: {}
@@ -0,0 +1,223 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.22.0
name: postgresqltenants.database.ayatori.ddupan.top
spec:
group: database.ayatori.ddupan.top
names:
kind: PostgreSQLTenant
listKind: PostgreSQLTenantList
plural: postgresqltenants
singular: postgresqltenant
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .status.databaseRef.name
name: Database
type: string
- jsonPath: .status.conditions[?(@.type=='Ready')].status
name: Ready
type: string
name: v1alpha1
schema:
openAPIV3Schema:
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: PostgreSQLTenantSpec 显式选择动态申请或已有 Database,不重复声明来源。
properties:
databaseRef:
description: DatabaseReference 是 Tenant 对已有集群级 PostgreSQLDatabase
的选择。
properties:
name:
description: ObjectName 定位集群级资源,不携带 namespace 或隐式跨 API group 引用。
maxLength: 253
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$
type: string
required:
- name
type: object
extensions:
description: Extensions 保留后端扩展名称的原样拼写,不按 SQL identifier 限制。
items:
type: string
type: array
x-kubernetes-list-type: set
provision:
description: DatabaseProvisionRequest 仅用于动态申请,省略名称时由 controller 按
Tenant 名称解析。
properties:
database:
description: PostgreSQLIdentifier 是第一版受管 database 与 login role
使用的名称。
maxLength: 63
pattern: ^[a-z][a-z0-9_]{0,62}$
type: string
instanceRef:
description: InstanceReference 仅引用同 API group 的集群级 PostgreSQLInstance。
properties:
name:
description: ObjectName 定位集群级资源,不携带 namespace 或隐式跨 API group
引用。
maxLength: 253
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$
type: string
required:
- name
type: object
loginRole:
description: PostgreSQLIdentifier 是第一版受管 database 与 login role
使用的名称。
maxLength: 63
pattern: ^[a-z][a-z0-9_]{0,62}$
type: string
required:
- instanceRef
type: object
secretName:
description: SecretName 指定 Tenant namespace 内的投射目标,省略时使用合同约定的默认名称。
maxLength: 253
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$
type: string
type: object
x-kubernetes-validations:
- message: exactly one of provision and databaseRef is required
rule: has(self.provision) != has(self.databaseRef)
status:
properties:
conditions:
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
credentialURL:
description: CredentialURL 只含 OpenBao API 位置,禁止嵌入认证信息。
type: string
databaseRef:
description: DatabaseRef 只有在资源侧确认绑定后才写入。
properties:
name:
description: ObjectName 定位集群级资源,不携带 namespace 或隐式跨 API group 引用。
maxLength: 253
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$
type: string
uid:
description: |-
UID is a type that holds unique ID values, including UUIDs. Because we
don't ONLY use UUIDs, this is an alias to string. Being a type captures
intent and helps make sure that UIDs and names do not get conflated.
maxLength: 128
minLength: 1
type: string
required:
- name
- uid
type: object
observedGeneration:
format: int64
type: integer
phase:
type: string
secretName:
description: SecretName 是已观察到的同 namespace 投射目标,不包含凭据值。
maxLength: 253
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$
type: string
type: object
required:
- spec
type: object
x-kubernetes-validations:
- message: binding target cannot change after binding starts
rule: '!has(oldSelf.status) || !has(oldSelf.status.phase) || !(oldSelf.status.phase
in [''Binding'', ''Bound'', ''Deleting'']) || ((has(self.spec.provision)
== has(oldSelf.spec.provision)) && (!has(oldSelf.spec.provision) || self.spec.provision
== oldSelf.spec.provision) && (has(self.spec.databaseRef) == has(oldSelf.spec.databaseRef))
&& (!has(oldSelf.spec.databaseRef) || self.spec.databaseRef == oldSelf.spec.databaseRef))'
- message: binding progress cannot return to an unbound state
rule: '!has(oldSelf.status) || !has(oldSelf.status.phase) || !(oldSelf.status.phase
in [''Binding'', ''Bound'', ''Deleting'']) || (has(self.status) && has(self.status.phase)
&& self.status.phase in [''Binding'', ''Bound'', ''Deleting''])'
served: true
storage: true
subresources:
status: {}
@@ -0,0 +1,307 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.22.0
name: jobclasses.execution.ayatori.ddupan.top
spec:
group: execution.ayatori.ddupan.top
names:
kind: JobClass
listKind: JobClassList
plural: jobclasses
singular: jobclass
scope: Cluster
versions:
- additionalPrinterColumns:
- jsonPath: .spec.controllerName
name: Controller
type: string
- jsonPath: .status.conditions[?(@.type=='Accepted')].status
name: Accepted
type: string
- jsonPath: .status.conditions[?(@.type=='Ready')].status
name: Ready
type: string
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1alpha1
schema:
openAPIV3Schema:
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
properties:
allowedNamespaces:
description: |-
A label selector is a label query over a set of resources. The result of matchLabels and
matchExpressions are ANDed. An empty label selector matches all objects. A null
label selector matches no objects.
properties:
matchExpressions:
description: matchExpressions is a list of label selector requirements.
The requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that the selector applies
to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
controllerName:
maxLength: 253
minLength: 1
type: string
parametersRef:
properties:
group:
type: string
kind:
type: string
name:
type: string
uid:
description: |-
UID is a type that holds unique ID values, including UUIDs. Because we
don't ONLY use UUIDs, this is an alias to string. Being a type captures
intent and helps make sure that UIDs and names do not get conflated.
type: string
required:
- group
- kind
- name
type: object
resources:
properties:
defaults:
properties:
limits:
properties:
cpu:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
memory:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
type: object
requests:
properties:
cpu:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
memory:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
type: object
type: object
maximum:
properties:
limits:
properties:
cpu:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
memory:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
type: object
requests:
properties:
cpu:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
memory:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
type: object
type: object
minimum:
properties:
limits:
properties:
cpu:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
memory:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
type: object
requests:
properties:
cpu:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
memory:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
type: object
type: object
type: object
required:
- controllerName
- parametersRef
type: object
x-kubernetes-validations:
- message: controllerName is immutable
rule: self.controllerName == oldSelf.controllerName
- message: parametersRef is immutable
rule: self.parametersRef == oldSelf.parametersRef
status:
properties:
conditions:
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
observedGeneration:
format: int64
type: integer
type: object
required:
- spec
type: object
served: true
storage: true
subresources:
status: {}
@@ -0,0 +1,408 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.22.0
name: jobs.execution.ayatori.ddupan.top
spec:
group: execution.ayatori.ddupan.top
names:
kind: Job
listKind: JobList
plural: jobs
singular: job
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .status.conditions[?(@.type=='Accepted')].status
name: Accepted
type: string
- jsonPath: .status.conditions[?(@.type=='Scheduled')].status
name: Scheduled
type: string
- jsonPath: .status.conditions[?(@.type=='Succeeded')].status
name: Succeeded
type: string
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1alpha1
schema:
openAPIV3Schema:
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
properties:
activeDeadlineSeconds:
format: int64
minimum: 1
type: integer
desiredState:
default: Running
enum:
- Running
- Cancelled
type: string
jobClassName:
type: string
resources:
properties:
limits:
properties:
cpu:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
memory:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
type: object
requests:
properties:
cpu:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
memory:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
type: object
type: object
task:
properties:
args:
items:
type: string
type: array
command:
items:
type: string
type: array
env:
items:
properties:
name:
pattern: ^[A-Za-z_][A-Za-z0-9_]*$
type: string
value:
type: string
valueFrom:
properties:
configMapKeyRef:
description: Selects a key from a ConfigMap.
properties:
key:
description: |-
The key to select from the ConfigMap's Data field.
Keys in the BinaryData field are not currently propagated to container env vars.
type: string
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
optional:
description: Specify whether the ConfigMap or its
key must be defined
type: boolean
required:
- key
type: object
x-kubernetes-map-type: atomic
secretKeyRef:
description: SecretKeySelector selects a key of a Secret.
properties:
key:
description: The key of the secret to select from. Must
be a valid secret key.
type: string
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
optional:
description: Specify whether the Secret or its key
must be defined
type: boolean
required:
- key
type: object
x-kubernetes-map-type: atomic
type: object
x-kubernetes-validations:
- message: exactly one key reference must be set
rule: has(self.secretKeyRef) != has(self.configMapKeyRef)
required:
- name
type: object
x-kubernetes-validations:
- message: exactly one of value or valueFrom must be set
rule: has(self.value) != has(self.valueFrom)
type: array
x-kubernetes-list-map-keys:
- name
x-kubernetes-list-type: map
image:
minLength: 1
type: string
imagePullSecrets:
items:
description: |-
LocalObjectReference contains enough information to let you locate the
referenced object inside the same namespace.
properties:
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
type: object
x-kubernetes-map-type: atomic
type: array
workingDir:
type: string
required:
- image
type: object
ttlSecondsAfterFinished:
format: int32
minimum: 0
type: integer
required:
- task
type: object
x-kubernetes-validations:
- message: task is immutable
rule: self.task == oldSelf.task
- message: resources are immutable
rule: self.resources == oldSelf.resources
- message: activeDeadlineSeconds is immutable
rule: has(self.activeDeadlineSeconds) == has(oldSelf.activeDeadlineSeconds)
&& (!has(self.activeDeadlineSeconds) || self.activeDeadlineSeconds
== oldSelf.activeDeadlineSeconds)
- message: jobClassName is immutable
rule: has(self.jobClassName) == has(oldSelf.jobClassName) && (!has(self.jobClassName)
|| self.jobClassName == oldSelf.jobClassName)
- message: desiredState may only transition from Running to Cancelled
rule: oldSelf.desiredState == self.desiredState || (oldSelf.desiredState
== 'Running' && self.desiredState == 'Cancelled')
status:
properties:
completionTime:
format: date-time
type: string
conditions:
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
effectiveResources:
properties:
limits:
properties:
cpu:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
memory:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
type: object
requests:
properties:
cpu:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
memory:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
type: object
type: object
execution:
properties:
adapter:
type: string
references:
items:
properties:
id:
minLength: 1
type: string
type:
minLength: 1
type: string
required:
- id
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
required:
- adapter
type: object
observedGeneration:
format: int64
type: integer
resolvedJobClass:
properties:
controllerName:
type: string
name:
type: string
parametersRef:
properties:
group:
type: string
kind:
type: string
name:
type: string
uid:
description: |-
UID is a type that holds unique ID values, including UUIDs. Because we
don't ONLY use UUIDs, this is an alias to string. Being a type captures
intent and helps make sure that UIDs and names do not get conflated.
type: string
required:
- group
- kind
- name
type: object
uid:
description: |-
UID is a type that holds unique ID values, including UUIDs. Because we
don't ONLY use UUIDs, this is an alias to string. Being a type captures
intent and helps make sure that UIDs and names do not get conflated.
type: string
required:
- controllerName
- name
- parametersRef
- uid
type: object
result:
properties:
exitCode:
format: int32
type: integer
reason:
type: string
type: object
startTime:
format: date-time
type: string
type: object
required:
- spec
type: object
served: true
storage: true
subresources:
status: {}
@@ -0,0 +1,339 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.22.0
name: kubernetesexecutionparameters.execution.ayatori.ddupan.top
spec:
group: execution.ayatori.ddupan.top
names:
kind: KubernetesExecutionParameters
listKind: KubernetesExecutionParametersList
plural: kubernetesexecutionparameters
singular: kubernetesexecutionparameters
scope: Cluster
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
properties:
imagePullPolicy:
default: IfNotPresent
description: PullPolicy describes a policy for if/when to pull a container
image
enum:
- Always
- Never
- IfNotPresent
type: string
podSecurityContext:
description: |-
PodSecurityContext holds pod-level security attributes and common container settings.
Some fields are also present in container.securityContext. Field values of
container.securityContext take precedence over field values of PodSecurityContext.
properties:
appArmorProfile:
description: |-
appArmorProfile is the AppArmor options to use by the containers in this pod.
Note that this field cannot be set when spec.os.name is windows.
properties:
localhostProfile:
description: |-
localhostProfile indicates a profile loaded on the node that should be used.
The profile must be preconfigured on the node to work.
Must match the loaded name of the profile.
Must be set if and only if type is "Localhost".
type: string
type:
description: |-
type indicates which kind of AppArmor profile will be applied.
Valid options are:
Localhost - a profile pre-loaded on the node.
RuntimeDefault - the container runtime's default profile.
Unconfined - no AppArmor enforcement.
type: string
required:
- type
type: object
fsGroup:
description: |-
A special supplemental group that applies to all containers in a pod.
Some volume types allow the Kubelet to change the ownership of that volume
to be owned by the pod:
1. The owning GID will be the FSGroup
2. The setgid bit is set (new files created in the volume will be owned by FSGroup)
3. The permission bits are OR'd with rw-rw----
If unset, the Kubelet will not modify the ownership and permissions of any volume.
Note that this field cannot be set when spec.os.name is windows.
format: int64
type: integer
fsGroupChangePolicy:
description: |-
fsGroupChangePolicy defines behavior of changing ownership and permission of the volume
before being exposed inside Pod. This field will only apply to
volume types which support fsGroup based ownership(and permissions).
It will have no effect on ephemeral volume types such as: secret, configmaps
and emptydir.
Valid values are "OnRootMismatch" and "Always". If not specified, "Always" is used.
Note that this field cannot be set when spec.os.name is windows.
type: string
runAsGroup:
description: |-
The GID to run the entrypoint of the container process.
Uses runtime default if unset.
May also be set in SecurityContext. If set in both SecurityContext and
PodSecurityContext, the value specified in SecurityContext takes precedence
for that container.
Note that this field cannot be set when spec.os.name is windows.
format: int64
type: integer
runAsNonRoot:
description: |-
Indicates that the container must run as a non-root user.
If true, the Kubelet will validate the image at runtime to ensure that it
does not run as UID 0 (root) and fail to start the container if it does.
If unset or false, no such validation will be performed.
May also be set in SecurityContext. If set in both SecurityContext and
PodSecurityContext, the value specified in SecurityContext takes precedence.
type: boolean
runAsUser:
description: |-
The UID to run the entrypoint of the container process.
Defaults to user specified in image metadata if unspecified.
May also be set in SecurityContext. If set in both SecurityContext and
PodSecurityContext, the value specified in SecurityContext takes precedence
for that container.
Note that this field cannot be set when spec.os.name is windows.
format: int64
type: integer
seLinuxChangePolicy:
description: |-
seLinuxChangePolicy defines how the container's SELinux label is applied to all volumes used by the Pod.
It has no effect on nodes that do not support SELinux or to volumes does not support SELinux.
Valid values are "MountOption" and "Recursive".
"Recursive" means relabeling of all files on all Pod volumes by the container runtime.
This may be slow for large volumes, but allows mixing privileged and unprivileged Pods sharing the same volume on the same node.
"MountOption" mounts all eligible Pod volumes with `-o context` mount option.
This requires all Pods that share the same volume to use the same SELinux label.
It is not possible to share the same volume among privileged and unprivileged Pods.
Eligible volumes are in-tree FibreChannel and iSCSI volumes, and all CSI volumes
whose CSI driver announces SELinux support by setting spec.seLinuxMount: true in their
CSIDriver instance. Other volumes are always re-labelled recursively.
If not specified, "MountOption" is used.
This field affects only Pods that have SELinux label set, either in PodSecurityContext or in SecurityContext of all containers.
All Pods that use the same volume should use the same seLinuxChangePolicy, otherwise some pods can get stuck in ContainerCreating state.
Note that this field cannot be set when spec.os.name is windows.
type: string
seLinuxOptions:
description: |-
The SELinux context to be applied to all containers.
If unspecified, the container runtime will allocate a random SELinux context for each
container. May also be set in SecurityContext. If set in
both SecurityContext and PodSecurityContext, the value specified in SecurityContext
takes precedence for that container.
Note that this field cannot be set when spec.os.name is windows.
properties:
level:
description: Level is SELinux level label that applies to
the container.
type: string
role:
description: Role is a SELinux role label that applies to
the container.
type: string
type:
description: Type is a SELinux type label that applies to
the container.
type: string
user:
description: User is a SELinux user label that applies to
the container.
type: string
type: object
seccompProfile:
description: |-
The seccomp options to use by the containers in this pod.
Note that this field cannot be set when spec.os.name is windows.
properties:
localhostProfile:
description: |-
localhostProfile indicates a profile defined in a file on the node should be used.
The profile must be preconfigured on the node to work.
Must be a descending path, relative to the kubelet's configured seccomp profile location.
Must be set if type is "Localhost". Must NOT be set for any other type.
type: string
type:
description: |-
type indicates which kind of seccomp profile will be applied.
Valid options are:
Localhost - a profile defined in a file on the node should be used.
RuntimeDefault - the container runtime default profile should be used.
Unconfined - no profile should be applied.
type: string
required:
- type
type: object
supplementalGroups:
description: |-
A list of groups applied to the first process run in each container, in
addition to the container's primary GID and fsGroup (if specified). If
the SupplementalGroupsPolicy feature is enabled, the
supplementalGroupsPolicy field determines whether these are in addition
to or instead of any group memberships defined in the container image.
If unspecified, no additional groups are added, though group memberships
defined in the container image may still be used, depending on the
supplementalGroupsPolicy field.
Note that this field cannot be set when spec.os.name is windows.
items:
format: int64
type: integer
type: array
x-kubernetes-list-type: atomic
supplementalGroupsPolicy:
description: |-
Defines how supplemental groups of the first container processes are calculated.
Valid values are "Merge" and "Strict". If not specified, "Merge" is used.
(Alpha) Using the field requires the SupplementalGroupsPolicy feature gate to be enabled
and the container runtime must implement support for this feature.
Note that this field cannot be set when spec.os.name is windows.
type: string
sysctls:
description: |-
Sysctls hold a list of namespaced sysctls used for the pod. Pods with unsupported
sysctls (by the container runtime) might fail to launch.
Note that this field cannot be set when spec.os.name is windows.
items:
description: Sysctl defines a kernel parameter to be set
properties:
name:
description: Name of a property to set
type: string
value:
description: Value of a property to set
type: string
required:
- name
- value
type: object
type: array
x-kubernetes-list-type: atomic
windowsOptions:
description: |-
The Windows specific settings applied to all containers.
If unspecified, the options within a container's SecurityContext will be used.
If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.
Note that this field cannot be set when spec.os.name is linux.
properties:
gmsaCredentialSpec:
description: |-
GMSACredentialSpec is where the GMSA admission webhook
(https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the
GMSA credential spec named by the GMSACredentialSpecName field.
type: string
gmsaCredentialSpecName:
description: GMSACredentialSpecName is the name of the GMSA
credential spec to use.
type: string
hostProcess:
description: |-
HostProcess determines if a container should be run as a 'Host Process' container.
All of a Pod's containers must have the same effective HostProcess value
(it is not allowed to have a mix of HostProcess containers and non-HostProcess containers).
In addition, if HostProcess is true then HostNetwork must also be set to true.
type: boolean
runAsUserName:
description: |-
The UserName in Windows to run the entrypoint of the container process.
Defaults to the user specified in image metadata if unspecified.
May also be set in PodSecurityContext. If set in both SecurityContext and
PodSecurityContext, the value specified in SecurityContext takes precedence.
type: string
type: object
type: object
runtimeClassName:
type: string
scheduling:
properties:
nodeSelector:
additionalProperties:
type: string
type: object
tolerations:
items:
description: |-
The pod this Toleration is attached to tolerates any taint that matches
the triple <key,value,effect> using the matching operator <operator>.
properties:
effect:
description: |-
Effect indicates the taint effect to match. Empty means match all taint effects.
When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.
type: string
key:
description: |-
Key is the taint key that the toleration applies to. Empty means match all taint keys.
If the key is empty, operator must be Exists; this combination means to match all values and all keys.
type: string
operator:
description: |-
Operator represents a key's relationship to the value.
Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal.
Exists is equivalent to wildcard for value, so that a pod can
tolerate all taints of a particular category.
Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators).
type: string
tolerationSeconds:
description: |-
TolerationSeconds represents the period of time the toleration (which must be
of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default,
it is not set, which means tolerate the taint forever (do not evict). Zero and
negative values will be treated as 0 (evict immediately) by the system.
format: int64
type: integer
value:
description: |-
Value is the taint value the toleration matches to.
If the operator is Exists, the value should be empty, otherwise just a regular string.
type: string
type: object
type: array
type: object
serviceAccountName:
minLength: 1
type: string
required:
- serviceAccountName
type: object
required:
- spec
type: object
served: true
storage: true
@@ -0,0 +1,83 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.22.0
name: opensandboxexecutionparameters.execution.ayatori.ddupan.top
spec:
group: execution.ayatori.ddupan.top
names:
kind: OpenSandboxExecutionParameters
listKind: OpenSandboxExecutionParametersList
plural: opensandboxexecutionparameters
singular: opensandboxexecutionparameters
scope: Cluster
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
properties:
allowImageAuth:
type: boolean
allowInsecureHTTP:
description: AllowInsecureHTTP is intended for isolated development
environments only.
type: boolean
allowSecretEnv:
type: boolean
apiKeySecretRef:
properties:
key:
type: string
name:
type: string
namespace:
type: string
required:
- key
- name
- namespace
type: object
endpoint:
minLength: 1
type: string
poolRef:
type: string
requestMapping:
default: AdmissionOnly
enum:
- AdmissionOnly
- Native
type: string
required:
- apiKeySecretRef
- endpoint
type: object
x-kubernetes-validations:
- message: endpoint must use HTTPS unless allowInsecureHTTP is true
rule: self.allowInsecureHTTP || self.endpoint.startsWith('https://')
required:
- spec
type: object
served: true
storage: true
+22
View File
@@ -0,0 +1,22 @@
# This kustomization.yaml is not intended to be run by itself,
# since it depends on service name and namespace that are out of this kustomize package.
# It should be run by config/default
resources:
- bases/database.ayatori.ddupan.top_postgresqlinstances.yaml
- bases/database.ayatori.ddupan.top_postgresqldatabases.yaml
- bases/database.ayatori.ddupan.top_postgresqltenants.yaml
- bases/execution.ayatori.ddupan.top_jobs.yaml
- bases/execution.ayatori.ddupan.top_jobclasses.yaml
- bases/execution.ayatori.ddupan.top_kubernetesexecutionparameters.yaml
- bases/execution.ayatori.ddupan.top_opensandboxexecutionparameters.yaml
# +kubebuilder:scaffold:crdkustomizeresource
patches:
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
# patches here are for enabling the conversion webhook for each CRD
# +kubebuilder:scaffold:crdkustomizewebhookpatch
# [WEBHOOK] To enable webhook, uncomment the following section
# the following config is for teaching kustomize how to do kustomization for CRDs.
#configurations:
#- kustomizeconfig.yaml
+12
View File
@@ -0,0 +1,12 @@
# This file is for teaching kustomize how to substitute name and namespace reference in CRD
nameReference:
- kind: Service
version: v1
fieldSpecs:
- kind: CustomResourceDefinition
version: v1
group: apiextensions.k8s.io
path: spec/conversion/webhook/clientConfig/service/name
varReference:
- path: metadata/annotations
@@ -0,0 +1,30 @@
# This patch adds the args, volumes, and ports to allow the manager to use the metrics-server certs.
# Add the volumeMount for the metrics-server certs
- op: add
path: /spec/template/spec/containers/0/volumeMounts/-
value:
mountPath: /tmp/k8s-metrics-server/metrics-certs
name: metrics-certs
readOnly: true
# Add the --metrics-cert-path argument for the metrics server
- op: add
path: /spec/template/spec/containers/0/args/-
value: --metrics-cert-path=/tmp/k8s-metrics-server/metrics-certs
# Add the metrics-server certs volume configuration
- op: add
path: /spec/template/spec/volumes/-
value:
name: metrics-certs
secret:
secretName: metrics-server-cert
optional: false
items:
- key: ca.crt
path: ca.crt
- key: tls.crt
path: tls.crt
- key: tls.key
path: tls.key
+233
View File
@@ -0,0 +1,233 @@
# Adds namespace to all resources.
namespace: ayatori-system
# Value of this field is prepended to the
# names of all resources, e.g. a deployment named
# "wordpress" becomes "alices-wordpress".
# Note that it should also match with the prefix (text before '-') of the namespace
# field above.
namePrefix: ayatori-
# Labels to add to all resources and selectors.
#labels:
#- includeSelectors: true
# pairs:
# someName: someValue
resources:
- ../crd
- ../rbac
- ../manager
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
#- ../webhook
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
#- ../certmanager
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
#- ../prometheus
# [METRICS] Expose the controller manager metrics service.
- metrics_service.yaml
# [NETWORK POLICY] Control ingress to metrics and webhook ports.
# Allow metrics traffic from pods in namespaces labeled 'metrics: enabled'.
# Allow webhook traffic from all sources.
#- ../network-policy
# Uncomment the patches line if you enable Metrics
patches:
# [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443.
# More info: https://book.kubebuilder.io/reference/metrics
- path: manager_metrics_patch.yaml
target:
kind: Deployment
# Uncomment the patches line if you enable Metrics and CertManager
# [METRICS-WITH-CERTS] To enable metrics protected with certManager, uncomment the following line.
# This patch will protect the metrics with certManager self-signed certs.
#- path: cert_metrics_manager_patch.yaml
# target:
# kind: Deployment
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
#- path: manager_webhook_patch.yaml
# target:
# kind: Deployment
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
# Uncomment the following replacements to add the cert-manager CA injection annotations
#replacements:
# - source: # Uncomment the following block to enable certificates for metrics
# kind: Service
# version: v1
# name: controller-manager-metrics-service
# fieldPath: metadata.name
# targets:
# - select:
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: metrics-certs
# fieldPaths:
# - spec.dnsNames.0
# - spec.dnsNames.1
# options:
# delimiter: '.'
# index: 0
# create: true
# - select: # Uncomment the following to set the Service name for TLS config in Prometheus ServiceMonitor
# kind: ServiceMonitor
# group: monitoring.coreos.com
# version: v1
# name: controller-manager-metrics-monitor
# fieldPaths:
# - spec.endpoints.0.tlsConfig.serverName
# options:
# delimiter: '.'
# index: 0
# create: true
# - source:
# kind: Service
# version: v1
# name: controller-manager-metrics-service
# fieldPath: metadata.namespace
# targets:
# - select:
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: metrics-certs
# fieldPaths:
# - spec.dnsNames.0
# - spec.dnsNames.1
# options:
# delimiter: '.'
# index: 1
# create: true
# - select: # Uncomment the following to set the Service namespace for TLS in Prometheus ServiceMonitor
# kind: ServiceMonitor
# group: monitoring.coreos.com
# version: v1
# name: controller-manager-metrics-monitor
# fieldPaths:
# - spec.endpoints.0.tlsConfig.serverName
# options:
# delimiter: '.'
# index: 1
# create: true
# - source: # Uncomment the following block if you have any webhook
# kind: Service
# version: v1
# name: webhook-service
# fieldPath: .metadata.name # Name of the service
# targets:
# - select:
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: serving-cert
# fieldPaths:
# - .spec.dnsNames.0
# - .spec.dnsNames.1
# options:
# delimiter: '.'
# index: 0
# create: true
# - source:
# kind: Service
# version: v1
# name: webhook-service
# fieldPath: .metadata.namespace # Namespace of the service
# targets:
# - select:
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: serving-cert
# fieldPaths:
# - .spec.dnsNames.0
# - .spec.dnsNames.1
# options:
# delimiter: '.'
# index: 1
# create: true
# - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation)
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: serving-cert # This name should match the one in certificate.yaml
# fieldPath: .metadata.namespace # Namespace of the certificate CR
# targets:
# - select:
# kind: ValidatingWebhookConfiguration
# fieldPaths:
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
# options:
# delimiter: '/'
# index: 0
# create: true
# - source:
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: serving-cert
# fieldPath: .metadata.name
# targets:
# - select:
# kind: ValidatingWebhookConfiguration
# fieldPaths:
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
# options:
# delimiter: '/'
# index: 1
# create: true
# - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting )
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: serving-cert
# fieldPath: .metadata.namespace # Namespace of the certificate CR
# targets:
# - select:
# kind: MutatingWebhookConfiguration
# fieldPaths:
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
# options:
# delimiter: '/'
# index: 0
# create: true
# - source:
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: serving-cert
# fieldPath: .metadata.name
# targets:
# - select:
# kind: MutatingWebhookConfiguration
# fieldPaths:
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
# options:
# delimiter: '/'
# index: 1
# create: true
# - source: # Uncomment the following block if you have a ConversionWebhook (--conversion)
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: serving-cert
# fieldPath: .metadata.namespace # Namespace of the certificate CR
# targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD.
# +kubebuilder:scaffold:crdkustomizecainjectionns
# - source:
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: serving-cert
# fieldPath: .metadata.name
# targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD.
# +kubebuilder:scaffold:crdkustomizecainjectionname
@@ -0,0 +1,4 @@
# This patch adds the args to allow exposing the metrics endpoint using HTTPS
- op: add
path: /spec/template/spec/containers/0/args/0
value: --metrics-bind-address=:8443
+18
View File
@@ -0,0 +1,18 @@
apiVersion: v1
kind: Service
metadata:
labels:
control-plane: controller-manager
app.kubernetes.io/name: ayatori
app.kubernetes.io/managed-by: kustomize
name: controller-manager-metrics-service
namespace: system
spec:
ports:
- name: https
port: 8443
protocol: TCP
targetPort: 8443
selector:
control-plane: controller-manager
app.kubernetes.io/name: ayatori
+2
View File
@@ -0,0 +1,2 @@
resources:
- manager.yaml
+107
View File
@@ -0,0 +1,107 @@
apiVersion: v1
kind: Namespace
metadata:
labels:
control-plane: controller-manager
app.kubernetes.io/name: ayatori
app.kubernetes.io/managed-by: kustomize
name: system
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller-manager
namespace: system
labels:
control-plane: controller-manager
app.kubernetes.io/name: ayatori
app.kubernetes.io/managed-by: kustomize
spec:
selector:
matchLabels:
control-plane: controller-manager
app.kubernetes.io/name: ayatori
replicas: 1
template:
metadata:
annotations:
kubectl.kubernetes.io/default-container: manager
labels:
control-plane: controller-manager
app.kubernetes.io/name: ayatori
spec:
# TODO(user): Uncomment the following code to configure the nodeAffinity expression
# according to the platforms which are supported by your solution.
# It is considered best practice to support multiple architectures. You can
# build your manager image using the makefile target docker-buildx.
# affinity:
# nodeAffinity:
# requiredDuringSchedulingIgnoredDuringExecution:
# nodeSelectorTerms:
# - matchExpressions:
# - key: kubernetes.io/arch
# operator: In
# values:
# - amd64
# - arm64
# - ppc64le
# - s390x
# - key: kubernetes.io/os
# operator: In
# values:
# - linux
securityContext:
# Projects are configured by default to adhere to the "restricted" Pod Security Standards.
# This ensures that deployments meet the highest security requirements for Kubernetes.
# For more details, see: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- command:
- /manager
args:
- --leader-elect
- --health-probe-bind-address=:8081
image: controller:latest
name: manager
env:
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
ports:
- containerPort: 8081
name: health
protocol: TCP
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop:
- "ALL"
livenessProbe:
httpGet:
path: /healthz
port: 8081
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
httpGet:
path: /readyz
port: 8081
initialDelaySeconds: 5
periodSeconds: 10
# TODO(user): Configure the resources accordingly based on the project requirements.
# More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
resources:
limits:
cpu: 500m
memory: 128Mi
requests:
cpu: 10m
memory: 64Mi
volumeMounts: []
volumes: []
serviceAccountName: controller-manager
terminationGracePeriodSeconds: 10
@@ -0,0 +1,26 @@
# Allow metrics traffic from pods in namespaces labeled 'metrics: enabled'.
# Add this label to namespaces whose pods should scrape metrics.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
labels:
app.kubernetes.io/name: ayatori
app.kubernetes.io/managed-by: kustomize
name: allow-metrics-traffic
namespace: system
spec:
podSelector:
matchLabels:
control-plane: controller-manager
app.kubernetes.io/name: ayatori
policyTypes:
- Ingress
ingress:
# Allow pods in namespaces labeled 'metrics: enabled' to scrape metrics.
- from:
- namespaceSelector:
matchLabels:
metrics: enabled # Only from namespaces with this label
ports:
- port: 8443
protocol: TCP
+2
View File
@@ -0,0 +1,2 @@
resources:
- allow-metrics-traffic.yaml
+11
View File
@@ -0,0 +1,11 @@
resources:
- monitor.yaml
# [PROMETHEUS-WITH-CERTS] The following patch configures the ServiceMonitor in ../prometheus
# to securely reference certificates created and managed by cert-manager.
# Additionally, ensure that you uncomment the [METRICS WITH CERTMANAGER] patch under config/default/kustomization.yaml
# to mount the "metrics-server-cert" secret in the Manager Deployment.
#patches:
# - path: monitor_tls_patch.yaml
# target:
# kind: ServiceMonitor
+27
View File
@@ -0,0 +1,27 @@
# Prometheus Monitor Service (Metrics)
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
labels:
control-plane: controller-manager
app.kubernetes.io/name: ayatori
app.kubernetes.io/managed-by: kustomize
name: controller-manager-metrics-monitor
namespace: system
spec:
endpoints:
- path: /metrics
port: https # Ensure this is the name of the port that exposes HTTPS metrics
scheme: https
bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
tlsConfig:
# TODO(user): The option insecureSkipVerify: true is not recommended for production since it disables
# certificate verification, exposing the system to potential man-in-the-middle attacks.
# For production environments, it is recommended to use cert-manager for automatic TLS certificate management.
# To apply this configuration, enable cert-manager and use the patch located at config/prometheus/servicemonitor_tls_patch.yaml,
# which securely references the certificate from the 'metrics-server-cert' secret.
insecureSkipVerify: true
selector:
matchLabels:
control-plane: controller-manager
app.kubernetes.io/name: ayatori
+19
View File
@@ -0,0 +1,19 @@
# Patch for Prometheus ServiceMonitor to enable secure TLS configuration
# using certificates managed by cert-manager
- op: replace
path: /spec/endpoints/0/tlsConfig
value:
# SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize
serverName: SERVICE_NAME.SERVICE_NAMESPACE.svc
insecureSkipVerify: false
ca:
secret:
name: metrics-server-cert
key: ca.crt
cert:
secret:
name: metrics-server-cert
key: tls.crt
keySecret:
name: metrics-server-cert
key: tls.key
@@ -0,0 +1,23 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: database-management-credentials
namespace: system
rules:
- apiGroups: [""]
resources: [secrets]
verbs: [get, list, watch]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: database-management-credentials
namespace: system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: database-management-credentials
subjects:
- kind: ServiceAccount
name: controller-manager
namespace: system
+27
View File
@@ -0,0 +1,27 @@
# This rule is not used by the project ayatori itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants full permissions ('*') over execution.ayatori.ddupan.top.
# This role is intended for users authorized to modify roles and bindings within the cluster,
# enabling them to delegate specific permissions to other users or groups as needed.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: ayatori
app.kubernetes.io/managed-by: kustomize
name: execution-job-admin-role
rules:
- apiGroups:
- execution.ayatori.ddupan.top
resources:
- jobs
verbs:
- '*'
- apiGroups:
- execution.ayatori.ddupan.top
resources:
- jobs/status
verbs:
- get
@@ -0,0 +1,33 @@
# This rule is not used by the project ayatori itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants permissions to create, update, and delete resources within the execution.ayatori.ddupan.top.
# This role is intended for users who need to manage these resources
# but should not control RBAC or manage permissions for others.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: ayatori
app.kubernetes.io/managed-by: kustomize
name: execution-job-editor-role
rules:
- apiGroups:
- execution.ayatori.ddupan.top
resources:
- jobs
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- execution.ayatori.ddupan.top
resources:
- jobs/status
verbs:
- get
@@ -0,0 +1,29 @@
# This rule is not used by the project ayatori itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants read-only access to execution.ayatori.ddupan.top resources.
# This role is intended for users who need visibility into these resources
# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: ayatori
app.kubernetes.io/managed-by: kustomize
name: execution-job-viewer-role
rules:
- apiGroups:
- execution.ayatori.ddupan.top
resources:
- jobs
verbs:
- get
- list
- watch
- apiGroups:
- execution.ayatori.ddupan.top
resources:
- jobs/status
verbs:
- get
@@ -0,0 +1,27 @@
# This rule is not used by the project ayatori itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants full permissions ('*') over execution.ayatori.ddupan.top.
# This role is intended for users authorized to modify roles and bindings within the cluster,
# enabling them to delegate specific permissions to other users or groups as needed.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: ayatori
app.kubernetes.io/managed-by: kustomize
name: execution-jobclass-admin-role
rules:
- apiGroups:
- execution.ayatori.ddupan.top
resources:
- jobclasses
verbs:
- '*'
- apiGroups:
- execution.ayatori.ddupan.top
resources:
- jobclasses/status
verbs:
- get
@@ -0,0 +1,33 @@
# This rule is not used by the project ayatori itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants permissions to create, update, and delete resources within the execution.ayatori.ddupan.top.
# This role is intended for users who need to manage these resources
# but should not control RBAC or manage permissions for others.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: ayatori
app.kubernetes.io/managed-by: kustomize
name: execution-jobclass-editor-role
rules:
- apiGroups:
- execution.ayatori.ddupan.top
resources:
- jobclasses
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- execution.ayatori.ddupan.top
resources:
- jobclasses/status
verbs:
- get
@@ -0,0 +1,29 @@
# This rule is not used by the project ayatori itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants read-only access to execution.ayatori.ddupan.top resources.
# This role is intended for users who need visibility into these resources
# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: ayatori
app.kubernetes.io/managed-by: kustomize
name: execution-jobclass-viewer-role
rules:
- apiGroups:
- execution.ayatori.ddupan.top
resources:
- jobclasses
verbs:
- get
- list
- watch
- apiGroups:
- execution.ayatori.ddupan.top
resources:
- jobclasses/status
verbs:
- get
@@ -0,0 +1,27 @@
# This rule is not used by the project ayatori itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants full permissions ('*') over execution.ayatori.ddupan.top.
# This role is intended for users authorized to modify roles and bindings within the cluster,
# enabling them to delegate specific permissions to other users or groups as needed.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: ayatori
app.kubernetes.io/managed-by: kustomize
name: execution-kubernetesexecutionparameters-admin-role
rules:
- apiGroups:
- execution.ayatori.ddupan.top
resources:
- kubernetesexecutionparameters
verbs:
- '*'
- apiGroups:
- execution.ayatori.ddupan.top
resources:
- kubernetesexecutionparameters/status
verbs:
- get
@@ -0,0 +1,33 @@
# This rule is not used by the project ayatori itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants permissions to create, update, and delete resources within the execution.ayatori.ddupan.top.
# This role is intended for users who need to manage these resources
# but should not control RBAC or manage permissions for others.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: ayatori
app.kubernetes.io/managed-by: kustomize
name: execution-kubernetesexecutionparameters-editor-role
rules:
- apiGroups:
- execution.ayatori.ddupan.top
resources:
- kubernetesexecutionparameters
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- execution.ayatori.ddupan.top
resources:
- kubernetesexecutionparameters/status
verbs:
- get
@@ -0,0 +1,29 @@
# This rule is not used by the project ayatori itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants read-only access to execution.ayatori.ddupan.top resources.
# This role is intended for users who need visibility into these resources
# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: ayatori
app.kubernetes.io/managed-by: kustomize
name: execution-kubernetesexecutionparameters-viewer-role
rules:
- apiGroups:
- execution.ayatori.ddupan.top
resources:
- kubernetesexecutionparameters
verbs:
- get
- list
- watch
- apiGroups:
- execution.ayatori.ddupan.top
resources:
- kubernetesexecutionparameters/status
verbs:
- get
@@ -0,0 +1,27 @@
# This rule is not used by the project ayatori itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants full permissions ('*') over execution.ayatori.ddupan.top.
# This role is intended for users authorized to modify roles and bindings within the cluster,
# enabling them to delegate specific permissions to other users or groups as needed.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: ayatori
app.kubernetes.io/managed-by: kustomize
name: execution-opensandboxexecutionparameters-admin-role
rules:
- apiGroups:
- execution.ayatori.ddupan.top
resources:
- opensandboxexecutionparameters
verbs:
- '*'
- apiGroups:
- execution.ayatori.ddupan.top
resources:
- opensandboxexecutionparameters/status
verbs:
- get
@@ -0,0 +1,33 @@
# This rule is not used by the project ayatori itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants permissions to create, update, and delete resources within the execution.ayatori.ddupan.top.
# This role is intended for users who need to manage these resources
# but should not control RBAC or manage permissions for others.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: ayatori
app.kubernetes.io/managed-by: kustomize
name: execution-opensandboxexecutionparameters-editor-role
rules:
- apiGroups:
- execution.ayatori.ddupan.top
resources:
- opensandboxexecutionparameters
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- execution.ayatori.ddupan.top
resources:
- opensandboxexecutionparameters/status
verbs:
- get
@@ -0,0 +1,29 @@
# This rule is not used by the project ayatori itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants read-only access to execution.ayatori.ddupan.top resources.
# This role is intended for users who need visibility into these resources
# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: ayatori
app.kubernetes.io/managed-by: kustomize
name: execution-opensandboxexecutionparameters-viewer-role
rules:
- apiGroups:
- execution.ayatori.ddupan.top
resources:
- opensandboxexecutionparameters
verbs:
- get
- list
- watch
- apiGroups:
- execution.ayatori.ddupan.top
resources:
- opensandboxexecutionparameters/status
verbs:
- get
+37
View File
@@ -0,0 +1,37 @@
resources:
# All RBAC will be applied under this service account in
# the deployment namespace. You may comment out this resource
# if your manager will use a service account that exists at
# runtime. Be sure to update RoleBinding and ClusterRoleBinding
# subjects if changing service account names.
- service_account.yaml
- role.yaml
- role_binding.yaml
- database_credentials_role.yaml
- leader_election_role.yaml
- leader_election_role_binding.yaml
# The following RBAC configurations are used to protect
# the metrics endpoint with authn/authz. These configurations
# ensure that only authorized users and service accounts
# can access the metrics endpoint. Comment the following
# permissions if you want to disable this protection.
# More info: https://book.kubebuilder.io/reference/metrics.html
- metrics_auth_role.yaml
- metrics_auth_role_binding.yaml
- metrics_reader_role.yaml
# For each CRD, "Admin", "Editor" and "Viewer" roles are scaffolded by
# default, aiding admins in cluster management. Those roles are
# not used by the ayatori itself. You can comment the following lines
# if you do not want those helpers be installed with your Project.
- execution_opensandboxexecutionparameters_admin_role.yaml
- execution_opensandboxexecutionparameters_editor_role.yaml
- execution_opensandboxexecutionparameters_viewer_role.yaml
- execution_kubernetesexecutionparameters_admin_role.yaml
- execution_kubernetesexecutionparameters_editor_role.yaml
- execution_kubernetesexecutionparameters_viewer_role.yaml
- execution_jobclass_admin_role.yaml
- execution_jobclass_editor_role.yaml
- execution_jobclass_viewer_role.yaml
- execution_job_admin_role.yaml
- execution_job_editor_role.yaml
- execution_job_viewer_role.yaml
+40
View File
@@ -0,0 +1,40 @@
# permissions to do leader election.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
labels:
app.kubernetes.io/name: ayatori
app.kubernetes.io/managed-by: kustomize
name: leader-election-role
rules:
- apiGroups:
- ""
resources:
- configmaps
verbs:
- get
- list
- watch
- create
- update
- patch
- delete
- apiGroups:
- coordination.k8s.io
resources:
- leases
verbs:
- get
- list
- watch
- create
- update
- patch
- delete
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
@@ -0,0 +1,15 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
labels:
app.kubernetes.io/name: ayatori
app.kubernetes.io/managed-by: kustomize
name: leader-election-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: leader-election-role
subjects:
- kind: ServiceAccount
name: controller-manager
namespace: system
+17
View File
@@ -0,0 +1,17 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: metrics-auth-role
rules:
- apiGroups:
- authentication.k8s.io
resources:
- tokenreviews
verbs:
- create
- apiGroups:
- authorization.k8s.io
resources:
- subjectaccessreviews
verbs:
- create
@@ -0,0 +1,12 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: metrics-auth-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: metrics-auth-role
subjects:
- kind: ServiceAccount
name: controller-manager
namespace: system
+9
View File
@@ -0,0 +1,9 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: metrics-reader
rules:
- nonResourceURLs:
- "/metrics"
verbs:
- get
+46
View File
@@ -0,0 +1,46 @@
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: manager-role
rules:
- apiGroups:
- database.ayatori.ddupan.top
resources:
- postgresqldatabases
verbs:
- create
- get
- list
- patch
- update
- watch
- apiGroups:
- database.ayatori.ddupan.top
resources:
- postgresqldatabases/finalizers
- postgresqlinstances/finalizers
- postgresqltenants/finalizers
verbs:
- update
- apiGroups:
- database.ayatori.ddupan.top
resources:
- postgresqldatabases/status
- postgresqlinstances/status
- postgresqltenants/status
verbs:
- get
- patch
- update
- apiGroups:
- database.ayatori.ddupan.top
resources:
- postgresqlinstances
- postgresqltenants
verbs:
- get
- list
- patch
- update
- watch
+15
View File
@@ -0,0 +1,15 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
labels:
app.kubernetes.io/name: ayatori
app.kubernetes.io/managed-by: kustomize
name: manager-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: manager-role
subjects:
- kind: ServiceAccount
name: controller-manager
namespace: system
+8
View File
@@ -0,0 +1,8 @@
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
app.kubernetes.io/name: ayatori
app.kubernetes.io/managed-by: kustomize
name: controller-manager
namespace: system
@@ -0,0 +1,37 @@
# 示例:只授权 controller 为固定登录 SA 创建短期 JWT;由管理员替换 namespace/subject 后应用。
# 不自动纳入 config/default,不包含 kubeconfig、长期 token 或 OpenBao 管理权限。
apiVersion: v1
kind: ServiceAccount
metadata:
name: database-openbao-login
namespace: ayatori-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: database-openbao-token
namespace: ayatori-system
rules:
- apiGroups: [""]
resources: [serviceaccounts/token]
resourceNames: [database-openbao-login]
verbs: [create]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: database-openbao-token
namespace: ayatori-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: database-openbao-token
subjects:
# 集群外示例:对应管理员签发 kubeconfig 的实际用户名,不是登录目标 SA 的名字。
- kind: User
apiGroup: rbac.authorization.k8s.io
name: ayatori-controller
# 集群内可以改为 manager 自己的 ServiceAccount;不要求两个 subject 同时授权。
# - kind: ServiceAccount
# name: controller-manager
# namespace: ayatori-system
@@ -0,0 +1,15 @@
# 管理员登记已有数据库;不会因创建 CR 就修改数据库或凭据。
apiVersion: database.ayatori.ddupan.top/v1alpha1
kind: PostgreSQLDatabase
metadata:
name: imported-app
spec:
instanceRef:
name: shared-postgres
database: existing_app
loginRole: existing_app
source: Import
credentialRef:
mount: secret
path: existing/app/postgresql
reclaimPolicy: Retain
@@ -0,0 +1,11 @@
# Instance 观察 controller 尚未接入;管理 Secret 由管理员在 controller namespace 提供。
apiVersion: database.ayatori.ddupan.top/v1alpha1
kind: PostgreSQLInstance
metadata:
name: shared-postgres
spec:
endpoint:
host: postgres.example.test
hostaddr: 192.0.2.10
adminCredentialRef:
name: shared-postgres-admin
@@ -0,0 +1,25 @@
# 二选一:动态申请或显式引用已有 Database;当前只有绑定协调,没有供应/交付 controller。
apiVersion: database.ayatori.ddupan.top/v1alpha1
kind: PostgreSQLTenant
metadata:
name: new-app
namespace: default
spec:
provision:
instanceRef:
name: shared-postgres
database: new_app
loginRole: new_app
extensions:
- pgcrypto
secretName: new-app-postgresql
---
apiVersion: database.ayatori.ddupan.top/v1alpha1
kind: PostgreSQLTenant
metadata:
name: existing-app
namespace: default
spec:
databaseRef:
name: imported-app
secretName: existing-app-postgresql
@@ -0,0 +1,14 @@
apiVersion: execution.ayatori.ddupan.top/v1alpha1
kind: Job
metadata:
labels:
app.kubernetes.io/name: ayatori
app.kubernetes.io/managed-by: kustomize
name: job-sample
spec:
jobClassName: default
task:
image: docker.io/library/alpine:3.22
command: ["/bin/sh", "-c"]
args: ["echo hello from Ayatori"]
ttlSecondsAfterFinished: 3600
@@ -0,0 +1,13 @@
apiVersion: execution.ayatori.ddupan.top/v1alpha1
kind: JobClass
metadata:
labels:
app.kubernetes.io/name: ayatori
app.kubernetes.io/managed-by: kustomize
name: default
spec:
controllerName: execution.ayatori.ddupan.top/kubernetes
parametersRef:
group: execution.ayatori.ddupan.top
kind: KubernetesExecutionParameters
name: default
@@ -0,0 +1,10 @@
apiVersion: execution.ayatori.ddupan.top/v1alpha1
kind: KubernetesExecutionParameters
metadata:
labels:
app.kubernetes.io/name: ayatori
app.kubernetes.io/managed-by: kustomize
name: default
spec:
serviceAccountName: ayatori-job
imagePullPolicy: IfNotPresent
@@ -0,0 +1,14 @@
apiVersion: execution.ayatori.ddupan.top/v1alpha1
kind: OpenSandboxExecutionParameters
metadata:
labels:
app.kubernetes.io/name: ayatori
app.kubernetes.io/managed-by: kustomize
name: microvm
spec:
endpoint: https://opensandbox-api.example.internal
apiKeySecretRef:
namespace: ayatori-system
name: opensandbox-api
key: api-key
requestMapping: AdmissionOnly
+10
View File
@@ -0,0 +1,10 @@
## Append samples of your project ##
resources:
- database_v1alpha1_postgresqlinstance.yaml
- database_v1alpha1_postgresqldatabase.yaml
- database_v1alpha1_postgresqltenant.yaml
- execution_v1alpha1_job.yaml
- execution_v1alpha1_jobclass.yaml
- execution_v1alpha1_kubernetesexecutionparameters.yaml
- execution_v1alpha1_opensandboxexecutionparameters.yaml
# +kubebuilder:scaffold:manifestskustomizesamples
+526
View File
@@ -0,0 +1,526 @@
# Job API v1alpha1 草案
- 状态:Draft
- 日期:2026-09-17
- API group:`execution.ayatori.ddupan.top`
- Kind:`Job`
- Scope:Namespaced
## 目标
`Job` 表达一次有限时长、有明确退出结果的机器执行。调用者描述任务载荷和资源需求,平台
选择 execution backend 并持续观察,直到任务成功、失败或取消。
首个 adapter 使用 Kubernetes `batch/v1 Job`,第二个 adapter 使用 OpenSandbox lifecycle
与 execd API。API 不暴露 PodSpec、sandbox ID 创建参数或具体 adapter 配置,但允许表达两个
真实后端共有的 OCI image、进程、环境变量和资源语义。
`Job` 是短生命周期控制对象。完成后依据 `ttlSecondsAfterFinished` 回收,长期业务状态由调用
者保存,日志由 observability 平台保存。详细保留策略见 ADR-0005。
## 非目标
v1alpha1 不提供:
- DAG、workflow 或多步骤 task;
- 定时执行与可复用 Job template;
- 并行 completions、indexed job 或 gang scheduling;
- 自动业务重试;
- 暂停后恢复;
- 交互式 shell、endpoint、snapshot 或长生命周期 sandbox;
- workspace、cache、artifact 上传协议或结构化 task outputs;
- 永久 Job history。
上述能力应由后续独立资源或经过真实需求验证的兼容字段提供,不能通过透传 PodSpec 或
OpenSandbox extensions 提前进入 API。
## 示例
```yaml
apiVersion: execution.ayatori.ddupan.top/v1alpha1
kind: Job
metadata:
generateName: hello-
namespace: ci
spec:
jobClassName: default
task:
image: docker.io/library/alpine:3.22
imagePullSecrets: []
command: ["/bin/sh", "-c"]
args:
- echo "hello ${TARGET}"
workingDir: /workspace
env:
- name: TARGET
value: world
- name: TOKEN
valueFrom:
secretKeyRef:
name: example-token
key: token
- name: CONFIG_VALUE
valueFrom:
configMapKeyRef:
name: example-config
key: value
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: "1"
memory: 512Mi
activeDeadlineSeconds: 600
ttlSecondsAfterFinished: 3600
desiredState: Running
```
## Spec
```yaml
spec:
jobClassName: string
task: TaskSpec
resources: ResourceRequirements
activeDeadlineSeconds: int64
ttlSecondsAfterFinished: int32
desiredState: Running | Cancelled
```
### `jobClassName`
可选。引用平台管理员维护的 cluster-scoped `JobClass`。调用者选择服务等级或执行
能力,而不是直接选择 adapter driver。省略时由 namespace policy 解析默认 class;不存在
默认值时 Job 保持未接受状态,不得静默选择任意后端。
一旦 Job 被接受,该字段不可变。解析出的实际 class 写入 status,以便默认策略后来变化时仍
能恢复原执行。
`JobClass` 及其强类型 parameters API 定义后端配置和调度策略。Job API 不暴露
Kubernetes namespace、OpenSandbox endpoint、API key 或 backend raw configuration。
### `task`
必填,创建后不可变。
```yaml
task:
image: string
imagePullSecrets: []LocalObjectReference
command: []string
args: []string
workingDir: string
env: []EnvVar
```
- `image`:必填 OCI image reference。首版允许 tag;生产策略可以通过 admission 要求 digest。
- `imagePullSecrets`:可选,同 namespace 的私有 registry 凭据引用。
- `command`:可选,覆盖 image entrypoint;空值表示使用 image 默认值。
- `args`:可选,传给 entrypoint/command。
- `workingDir`:可选;为空时使用 image/backend 默认值。
- `env`:可选,名称必须唯一。
`command` 和 `args` 使用 argv 语义,不隐式经过 shell。需要 shell 展开时,调用者必须显式
指定 `/bin/sh -c` 等命令。
#### 环境变量
```yaml
- name: EXAMPLE
value: literal
- name: TOKEN
valueFrom:
secretKeyRef:
name: example
key: token
optional: false
```
`value` 与 `valueFrom` 必须且只能设置一个。v1alpha1 支持同 namespace 的 `SecretKeyRef` 和
`ConfigMapKeyRef`,两者具有相同的引用、optional 和等待语义。Adapter 负责以适合后端且不
写入 Job status 的方式传递值。引用对象或 key 缺失时 Job 保持未开始并通过 Condition 报告。
Secret 内容不得复制到 Event、日志或 backend reference;ConfigMap 值虽然不视为机密,也不
写入 status,避免状态膨胀和不同后端行为不一致。
Kubernetes adapter 保留原生 `SecretKeyRef`/`ConfigMapKeyRef`,由 kubelet 在执行节点解析,
controller 不读取内容。OpenSandbox create API 只接收已经解析的环境变量值,因此该 adapter
必须读取引用并把值放入 sandbox create request。JobClass 必须明确允许 Secret 的这种
传递路径,且 controller 的日志、Event 和 status 不得记录请求正文。未来需要避免把真实凭据
暴露给 sandbox 进程时,使用 OpenSandbox Credential Vault 或 Ayatori 独立 Credential 能力,
而不是改变 `SecretKeyRef` 的既有语义。
私有镜像凭据采用 Kubernetes `kubernetes.io/dockerconfigjson` Secret。Kubernetes adapter
直接传递引用;OpenSandbox adapter 选择与目标 image registry 匹配的条目,并映射到其
`image.auth` create 参数。无法解析、没有匹配 registry 或所选 OpenSandbox runtime 不支持
per-request image auth 时,Job 以明确 reason 失败,不得退回匿名拉取后隐藏真实原因。
### `resources`
可选,使用 Kubernetes `resource.Quantity` 表示数值,但不复用完整 Pod
`ResourceRequirements` 行为。
v1alpha1 支持 `cpu` 和 `memory` 的 requests/limits。Requests 表达准入与调度需求,limits
表达执行上限。JobClass 可以提供默认值和允许范围;解析后的实际资源写入 status。
Adapter 必须显式验证能否满足请求,不能无提示地忽略 limit。后端无法区分 request 与 limit
时,其映射规则属于 JobClass,并在 Job 接受前确定。对 OpenSandbox,CPU 和内存
limits 直接映射为 sandbox VM/container 的 `resourceLimits`;requests 用于 Ayatori 的准入与
调度,并可由 JobClass 映射到后端 resource request 或 capacity profile。映射失败必须
显式拒绝或失败,不能静默降低资源保证。
### `activeDeadlineSeconds`
可选,必须大于零。表示从实际执行开始到任务必须终止的最长时间,不包含排队、class 解析或
后端 provisioning 时间。到期后 controller 请求终止后端,最终以 `Succeeded=False`、
`reason=DeadlineExceeded` 结束。
后端自身的 timeout 可以作为执行机制,但 Ayatori controller 仍以 `status.startTime` 和观察
结果维护领域语义。调度等待超时是不同概念,v1alpha1 不提供。
### `ttlSecondsAfterFinished`
可选,必须大于或等于零。语义与 Kubernetes Job 一致:从终态 transition time 起计算,零
表示立即具备删除资格。该字段在任务完成前后均可修改,但不能保证在既有 TTL 已过期后通过
延长 TTL 阻止并发删除。
平台应通过 schema、CEL 或 policy 设置最大值和推荐默认值。controller 本身不偷偷填充一个
无法从 spec 观察到的永久策略。
### `desiredState`
可选,默认 `Running`。允许的状态迁移只有:
```text
Running → Cancelled
```
设置 `Cancelled` 表示请求终止当前执行并保留 Job 至 TTL 到期。取消是尽力而为的异步操作;
只有 adapter 确认执行不会继续后,Job 才进入终态。字段不得从 `Cancelled` 改回 `Running`。
重新执行必须创建新的 Job。
v1alpha1 不提供 suspend/resume。对任意后端可靠实现 checkpoint/resume 并非共同能力,且暂停
不应被伪装为取消。
## 不可变性
创建后仅允许修改:
- `spec.desiredState`,且只能单向变为 `Cancelled`;
- `spec.ttlSecondsAfterFinished`。
`task`、`resources`、`activeDeadlineSeconds` 和 `jobClassName` 均不可变。首选 CRD CEL
validation 表达这些约束;只有 schema/CEL 无法正确表达时才引入 admission webhook。
Controller reconcile 的技术重试不表示任务重跑。v1alpha1 每个 Job 最多启动一个逻辑执行;
adapter 必须使用 Job UID 作为幂等键。若请求结果未知,controller 必须先 Observe,不能因为
网络超时重新创建可能已经开始的执行。
首个 Kubernetes adapter 创建 `backoffLimit: 0`、`restartPolicy: Never` 的原生 Job,避免继承
Kubernetes 默认的多次业务执行语义。需要重新执行时创建新的 Ayatori Job。
## Status
```yaml
status:
observedGeneration: 1
conditions:
- type: Accepted
status: "True"
reason: Valid
observedGeneration: 1
lastTransitionTime: ...
- type: Scheduled
status: "True"
reason: BackendCreated
observedGeneration: 1
lastTransitionTime: ...
- type: Succeeded
status: "Unknown"
reason: Running
observedGeneration: 1
lastTransitionTime: ...
resolvedJobClass:
name: default
uid: 8aa4...
controllerName: execution.ayatori.ddupan.top/kubernetes
parametersRef:
group: execution.ayatori.ddupan.top
kind: KubernetesExecutionParameters
name: default
uid: c413...
effectiveResources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: "1"
memory: 512Mi
execution:
adapter: kubernetes
references:
- type: Job
id: 5cb0...
startTime: ...
completionTime: ...
result:
exitCode: 0
reason: Completed
```
### Conditions
使用标准 `metav1.Condition`。v1alpha1 定义三个核心 Condition:
- `Accepted`:spec、引用、policy 和 JobClass 已解析,可进入调度;
- `Scheduled`:后端已确定并存在可观察的逻辑执行;
- `Succeeded`:任务结果。`Unknown` 表示尚未结束,`True` 表示成功,`False` 表示已经失败或
取消。
失败不使用单独的 `Failed` Condition。`Succeeded=True` 与 `Failed=True` 会形成需要额外维护的
互斥状态,而标准三态 Condition 已能完整表达一次执行:运行中为 `Unknown`、成功为 `True`、
失败为 `False`。失败类型由稳定 reason 区分;这与 Tekton `TaskRun` 的状态约定一致。
不增加与 Conditions 重复的 `phase` 字段。面向 CLI 的阶段摘要由 printer columns 或客户端从
Conditions 推导,避免两个状态源发生漂移。
常用 `Succeeded` reason 初始包括:
- `Pending`、`Scheduling`、`Running`;
- `Completed`;
- `ProcessFailed`;
- `DeadlineExceeded`;
- `Cancelled`;
- `BackendLost`;
- `ResultUnknown`。
Reason 是稳定、机器可读的 PascalCase 标识;message 面向人类且不得承载程序逻辑。
## 状态机
状态机名称用于设计、测试和 metrics,不增加持久化 `status.phase`。当前状态必须能够从 spec、
deletionTimestamp、Conditions、时间和 execution references 唯一推导。
### 状态定义
| 状态 | 判定摘要 | 含义 |
|---|---|---|
| `Resolving` | `Accepted!=True`,非终态 | 等待 JobClass、Secret、ConfigMap 或 policy 解析 |
| `Scheduling` | `Accepted=True`、`Scheduled!=True` | 选择 adapter 并幂等创建后端执行 |
| `Starting` | `Scheduled=True`、无 `startTime` | 后端已存在,任务主体尚未确认开始 |
| `Running` | 有 `startTime`、`Succeeded=Unknown` | 任务主体正在执行 |
| `Cancelling` | `desiredState=Cancelled`、非终态 | 正在确认后端已经停止 |
| `ResultUnknown` | `Succeeded=Unknown/ResultUnknown` | 无法证明任务仍在运行或已经停止 |
| `Succeeded` | `Succeeded=True` | 成功终态 |
| `Failed` | `Succeeded=False`,reason 非 `Cancelled` | 失败终态 |
| `Cancelled` | `Succeeded=False/Cancelled` | 取消终态 |
| `Deleting` | 存在 `deletionTimestamp` | finalizer 正在停止并清理后端,覆盖其他状态 |
存在多个判定条件时按 `Deleting → terminal → Cancelling → ResultUnknown → Running → Starting →
Scheduling → Resolving` 的优先级推导,保证状态唯一。
`ResultUnknown` 不是终态,不设置 `completionTime`,也不启动 TTL。只有确认任务已经停止,才能
转为成功、失败或取消。暂时无法联系后端不等于后端执行失败。
### 正常转移
```text
Resolving
│ 引用与策略解析完成
▼
Scheduling
│ 后端逻辑执行已建立并持久化引用
▼
Starting
│ adapter 确认任务主体开始
▼
Running ───────────────→ Succeeded
└──────────────────→ Failed
```
后端在任务主体开始前就确定失败,例如 image pull、runtime 不兼容或 provisioning 失败,可以从
`Scheduling` 或 `Starting` 直接进入 `Failed`,此时 `startTime` 允许为空。
### 取消转移
```text
Resolving ─┐
Scheduling ─┤
Starting ─┼→ Cancelling → Cancelled
Running ─┤
ResultUnknown ─┘
```
尚未创建后端执行时,取消可以立即确认。已经存在或可能存在后端执行时,必须反复执行
Cancel/Observe,确认不会继续运行后才能进入 `Cancelled`。取消请求与成功完成并发时,以先从
后端确认到的不可逆事实为准:已经成功完成的任务保持 `Succeeded`,不能改写成 `Cancelled`。
### 不确定结果与恢复
```text
Ensure/Observe 返回歧义
↓
ResultUnknown
├── 找回执行 → Starting / Running
├── 找到终态 → Succeeded / Failed / Cancelled
└── 管理员确认无法继续 → Failed(BackendLost)
```
在 `Ensure` 请求超时且尚未成功写入 external reference 时,adapter 必须使用 Job UID 查询后端,
不能直接再次创建。Controller 重启后遵循相同规则。
### 删除与 TTL 转移
任意状态收到 deletionTimestamp 后进入 `Deleting`。若执行可能存在,先 Cancel/Observe,再
Delete 后端资源并移除 finalizer。TTL controller 只对具有 `completionTime` 的三个终态发起
删除;`Resolving`、`Scheduling`、`Starting`、`Running`、`Cancelling` 和 `ResultUnknown` 均不
具备 TTL 删除资格。
### 状态不变量
- `Succeeded=True/False` 是不可逆终态;终态 reason、`completionTime` 和 result 不再改变。
- `startTime` 和 `completionTime` 一旦设置不可改变;两者都存在时 completionTime 不早于
startTime。
- `Succeeded=True` 必须具有 `completionTime`,可以没有 exit code,但 adapter 应说明原因。
- `Succeeded=False` 必须具有 `completionTime`;进程失败且能取得退出码时必须保存 exit code。
- 非终态的 `Succeeded` 必须为 `Unknown`,不得省略为具有歧义的空状态。
- `Scheduled=True` 前不得设置 `startTime`;一旦为 True 不再回退。
- execution references 只能由 controller 写入;已有引用不能静默替换成新的逻辑执行。
- Job UID 是执行幂等键;同名但不同 UID 的 Job 必须被视为不同执行。
- `desiredState=Cancelled` 后不得创建新的后端执行。
- reconcile 错误和退避不得修改任务的业务结果。
### 状态机测试矩阵
实现必须至少覆盖以下 table-driven unit tests,并为关键恢复路径提供 envtest:
| 类别 | 场景 | 必要断言 |
|---|---|---|
| 正常 | 创建、开始、退出 0 | 单次 Ensure,时间与成功终态正确 |
| 正常 | 主进程非零退出 | `Succeeded=False/ProcessFailed` 与 exit code |
| 解析 | JobClass 后创建 | 不提前 Ensure,引用出现后继续 |
| 解析 | Secret/ConfigMap 或 key 后创建 | 不泄露值,解析后只启动一次 |
| 后端 | image pull 或 provisioning 失败 | 未设置 startTime 的失败终态合法 |
| 幂等 | Ensure 成功但 status 写入前崩溃 | 通过 UID 找回,不能创建第二次执行 |
| 幂等 | 重复 reconcile 与重复事件 | 不产生额外执行,不改变终态时间 |
| 恢复 | controller 在各非终态重启 | 从持久 status/reference 恢复正确状态 |
| 未知 | Ensure/Observe 超时且结果不明 | 保持非终态,不设 completionTime,不触发 TTL |
| 未知 | 后端恢复后找回运行任务 | 从 ResultUnknown 返回 Running |
| 未知 | 管理员确认执行丢失 | 只在确认后进入 `Failed/BackendLost` |
| 取消 | 在解析、调度、启动、运行阶段取消 | 不再创建或确认停止后才进入 Cancelled |
| 竞态 | 取消与成功完成并发 | 已确认成功不被取消覆盖 |
| 超时 | active deadline 到期 | 请求取消,确认停止后 `DeadlineExceeded` |
| 删除 | 每个非终态阶段删除 | finalizer 清理完成前对象不消失 |
| 删除 | 后端暂时不可达 | finalizer 保留并重试,不误报已清理 |
| TTL | 三种终态到期 | 到期前不删,到期后带 UID precondition 删除 |
| TTL | controller 在等待 TTL 时重启 | informer 恢复计时,最终删除一次 |
| TTL | 到期附近延长 TTL | 最终 GET 重新核对最新 TTL |
| 隔离 | 同名 Job 删除并以新 UID 重建 | 旧队列项和旧后端不得影响新 Job |
| 校验 | 修改不可变字段或取消后恢复 Running | schema/CEL 拒绝请求 |
| 引用 | OpenSandbox 保存 Sandbox 与 Command 引用 | 顺序重试后引用稳定且无凭据 |
### 时间
- `startTime`:adapter 确认任务主体开始执行的时间,而不是 CR 创建或 backend provisioning
时间;设置后不可改变。
- `completionTime`:进入最终成功、失败或取消状态的时间;设置后不可改变。
TTL 以 `completionTime` 为基准。若后端已经完成但结果暂时无法确认,不得猜测 completionTime。
### Execution reference
`status.execution` 是 execution 领域定义的正式 API 字段,保存 controller 重启后重新 Observe
所需的最小稳定引用:
- `adapter`:实际 adapter 类型;
- `references`:一个或多个由 adapter 定义的不透明外部引用。
```yaml
execution:
adapter: opensandbox
references:
- type: Sandbox
id: sandbox-123
- type: Command
id: command-456
```
单个 `externalID` 不足以表达 OpenSandbox 的 sandbox 与 command 两级资源。`type` 和 `id` 的
值由对应 adapter 定义,调用者只能用于诊断和关联,不能据此实现领域逻辑。execution 领域将
每个引用限制为 `type` 与 `id` 两个非空、有长度上限的字符串,不提供任意 metadata map 或
raw JSON。引用不包含 endpoint、凭据或 Secret 内容。Kubernetes adapter 可以另外通过 owner
reference 管理原生 Job,但仍需把恢复所需引用持久化,并保证同名重建安全。
该结构不提升为跨领域共享的万能 ExternalReference。VM、数据库和 LB 等领域根据真实后端
需要定义自己的受限引用 schema,只有多个领域出现语义完全一致的实际重复后才考虑共享。
### Result
`result.exitCode` 只在后端能够确定主进程退出码时设置。调度失败、取消、后端丢失等情况可以
没有退出码。`result.reason` 提供简短分类;详细诊断写入 Condition message 和 observability,
不得把完整日志写入 status。
Job UID 是跨后端日志、metrics 和 traces 的主要 correlation identity。Adapter 必须将
namespace、name 和 UID 传入执行环境或后端 metadata;高基数字段如何索引由 observability
平台决定,API 不要求把 UID 配置为日志 label。
## 删除与 finalizer
Execution controller 在可能创建外部执行前添加
`execution.ayatori.ddupan.top/job-cleanup` finalizer。
删除一个活动 Job 表示取消并清理,而不是 orphan:
1. 请求 adapter 终止执行;
2. Observe,确认执行不会继续;
3. 删除后端临时资源与短期凭据;
4. 移除 finalizer。
首版不提供用户可选 orphan policy。让一次性任务脱离控制面继续运行既难以观察,也可能产生
副作用。后端长期不可达时由管理员根据 runbook 判断并强制移除 finalizer,该操作必须可审计。
TTL controller 只发起 Job 删除,所有手工删除和 TTL 删除都经过相同 finalizer 路径。
## Adapter contract 对 API 的保证
每个 execution adapter 必须提供以下语义,而非暴露自身 SDK 类型:
```text
Ensure 幂等地建立以 Job UID 标识的一个逻辑执行
Observe 返回尚未开始、运行、成功、失败、取消或结果未知
Cancel 请求停止且可被重复调用
Delete 清理后端临时资源且可被重复调用
```
`Ensure` 的网络超时不能直接触发第二次执行。Adapter 必须能够通过 UID/metadata 查找已创建的
后端对象,或返回 `ResultUnknown` 交由人工处理。
Kubernetes adapter 与 OpenSandbox adapter 实现后,应复审 contract 和 API。只有两个真实
实现都需要且语义相同的字段才提升为通用能力;后端特有功能优先进入 JobClass 或独立
资源,不增加 `rawConfig`。
OpenSandbox 支持从 OCI image 创建 sandbox,但不保证每个 image 都能在所选 runtime、架构或
安全 profile 下成功启动。Adapter 对已知不支持的组合应尽早报告;image pull、进程启动或
运行时不兼容等实际后端失败最终统一表现为 `Succeeded=False`,并以 reason/message 保留可
诊断原因。这不要求 Ayatori 在提交前证明任意 OCI image 一定可运行。
## 待后续设计
- capability-based class 自动选择;
- 私有 image registry 的凭据和统一 workload identity;
- artifact、workspace 与 cache 的独立 API;
- Job 创建速率、并发、quota、公平调度以及是否集成 Kueue;
- observability correlation 的具体 OpenTelemetry/Loki 字段约定;
- 调用者错过 TTL 时是否需要可选的最小审计记录。
这些问题不阻塞首个 Kubernetes adapter 的 API review;image pull 的最小凭据路径必须在实现
前通过 Kubernetes 与 OpenSandbox adapter 测试验证。
## 成熟实现参考
- [Kubernetes Job](https://kubernetes.io/docs/concepts/workloads/controllers/job/)
- [Kubernetes Job API](https://kubernetes.io/docs/reference/kubernetes-api/batch/job-v1/)
- [Tekton Pipeline API](https://tekton.dev/docs/pipelines/pipeline-api/)
- [Kueue Workload](https://kueue.sigs.k8s.io/docs/concepts/workload/)
- [OpenSandbox API specifications](https://github.com/opensandbox-group/OpenSandbox/blob/main/docs/api/index.md)
+411
View File
@@ -0,0 +1,411 @@
# JobClass API v1alpha1 草案
- 状态:Draft
- 日期:2026-09-17
- API group:`execution.ayatori.ddupan.top`
- Kind:`JobClass`
- Scope:Cluster
## 目标
`JobClass` 是平台管理员提供给 Job 调用者的执行服务等级。名称表达稳定的用户语义,
例如 `default`、`rootless`、`microvm` 或 `trusted-infra`;调用者不需要知道它当前由 Kubernetes
还是 OpenSandbox 实现。
JobClass 负责:
- 选择拥有该 class 的 adapter/controller;
- 引用 adapter 自己的强类型参数对象;
- 限制允许使用该 class 的 namespace;
- 提供跨后端一致的资源默认值与范围;
- 向 Job controller 报告配置是否被接受、后端是否可用。
它不负责保存队列状态、并发配额、Job history 或任意后端 raw config。
## 设计依据
- Kubernetes `RuntimeClass` 使用 cluster-scoped class 将调用者与具体 runtime handler、调度约束
和 overhead 隔离。
- `StorageClass` 允许管理员用稳定名称提供不同服务等级,并由调用者显式或默认选择。
- Gateway API `GatewayClass` 使用 `controllerName + parametersRef` 将稳定 class API 与实现专用
参数分离,并通过 `Accepted` Condition 报告配置有效性。
- Kueue `ResourceFlavor` 将资源规格与具体节点标签、taint 等 placement 细节分开。
Ayatori 采用 GatewayClass 风格的参数引用,不在 JobClass 中建立随 adapter 数量膨胀的
union,也不使用 `map[string]any`。
## 示例
### Kubernetes execution
```yaml
apiVersion: execution.ayatori.ddupan.top/v1alpha1
kind: JobClass
metadata:
name: rootless
annotations:
execution.ayatori.ddupan.top/is-default-job-class: "true"
spec:
controllerName: execution.ayatori.ddupan.top/kubernetes
parametersRef:
group: execution.ayatori.ddupan.top
kind: KubernetesExecutionParameters
name: rootless
allowedNamespaces:
matchLabels:
ayatori.ddupan.top/execution: enabled
resources:
defaults:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: "2"
memory: 2Gi
maximum:
limits:
cpu: "8"
memory: 16Gi
---
apiVersion: execution.ayatori.ddupan.top/v1alpha1
kind: KubernetesExecutionParameters
metadata:
name: rootless
spec:
serviceAccountName: ayatori-job
runtimeClassName: runc
scheduling:
nodeSelector:
ayatori.ddupan.top/node-role: execution
tolerations:
- key: ayatori.ddupan.top/execution
operator: Equal
value: "true"
effect: NoSchedule
podSecurityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
```
### OpenSandbox execution
```yaml
apiVersion: execution.ayatori.ddupan.top/v1alpha1
kind: JobClass
metadata:
name: microvm
spec:
controllerName: execution.ayatori.ddupan.top/opensandbox
parametersRef:
group: execution.ayatori.ddupan.top
kind: OpenSandboxExecutionParameters
name: microvm
allowedNamespaces:
matchLabels:
ayatori.ddupan.top/microvm-access: "true"
resources:
defaults:
requests:
cpu: "1"
memory: 1Gi
limits:
cpu: "2"
memory: 2Gi
maximum:
limits:
cpu: "8"
memory: 16Gi
---
apiVersion: execution.ayatori.ddupan.top/v1alpha1
kind: OpenSandboxExecutionParameters
metadata:
name: microvm
spec:
endpoint: https://opensandbox-api.example.internal
apiKeySecretRef:
namespace: ayatori-system
name: opensandbox-api
key: api-key
poolRef: microvm
requestMapping: AdmissionOnly
allowSecretEnv: true
allowImageAuth: true
allowInsecureHTTP: false
```
`endpoint` 默认必须使用 HTTPS。隔离的本地开发环境可以显式设置
`allowInsecureHTTP: true` 使用 HTTP;生产配置不得启用该开关。
## JobClass spec
```yaml
spec:
controllerName: string
parametersRef: ParametersReference
allowedNamespaces: LabelSelector
resources: ExecutionResourcePolicy
```
### `controllerName`
必填、创建后不可变。使用 domain-prefixed path 标识负责处理该 class 和 Job 的 controller,例如:
```text
execution.ayatori.ddupan.top/kubernetes
execution.ayatori.ddupan.top/opensandbox
```
它是 controller 所有权标识,不是任意可执行插件名称。一个 controller 只能处理自己明确支持
的名称。未来 adapter 拆成独立 Deployment 时,class 和 Job API 无需改变。
### `parametersRef`
必填、创建后不可变:
```yaml
parametersRef:
group: execution.ayatori.ddupan.top
kind: KubernetesExecutionParameters
name: rootless
```
v1alpha1 只允许引用 cluster-scoped 参数对象,并限制 `group`、`kind`、`name` 的长度与格式。
每个 controller 明确列出支持的 kind;引用 ConfigMap、Secret 或未知 CRD 不被接受。
参数引用只有一层,参数 CRD 可以进一步引用 Secret 等运行配置。禁止嵌套通用参数链和 raw
JSON,避免 class 成为无法校验的配置转发器。
### `allowedNamespaces`
可选 Kubernetes `LabelSelector`。Job 所在 namespace 必须匹配才可使用该 class。省略表示允许
所有 namespace;这是显式的管理员选择,而不是用户能力。
该检查由 Job controller 执行,并应尽可能增加 CEL/admission policy 作为快速反馈。用户即使
知道 privileged class 名称,也不能仅靠设置 `jobClassName` 绕过授权。
Namespace label 在 Job 被接受后发生变化,不中断已经运行的 Job,但影响新的 Job。紧急终止
使用独立管理员操作,不通过修改 selector 隐式杀死任务。
### `resources`
可选,定义后端无关的 CPU、内存策略:
```yaml
resources:
defaults:
requests: {cpu, memory}
limits: {cpu, memory}
minimum:
requests: {cpu, memory}
limits: {cpu, memory}
maximum:
requests: {cpu, memory}
limits: {cpu, memory}
```
规则为:
1. Job 未设置的值由 defaults 补齐;
2. 解析结果必须满足 minimum/maximum;
3. CPU 与内存 request 不得大于对应 limit;
4. 解析后的 effective resources 写入 Job status;
5. 后续修改 class 不改变已经接受的 Job;
6. adapter 不能静默降低 effective resources。
只支持 CPU 和内存。GPU、临时磁盘等资源在出现真实后端需求后增加,不先复制完整 Kubernetes
ResourceList。
Runtime/VM overhead 是 adapter 参数或后端调度实现,不计入用户请求的 task resources。
Kubernetes adapter 应优先利用 RuntimeClass Pod overhead;OpenSandbox adapter 在其 capacity
profile 中计算 microVM overhead。
## 默认 class 选择
Job 显式设置 `spec.jobClassName` 时始终优先使用该值。省略时按以下顺序解析:
1. Job namespace annotation
`execution.ayatori.ddupan.top/default-job-class`;
2. 唯一带有
`execution.ayatori.ddupan.top/is-default-job-class: "true"` annotation 的 JobClass。
若不存在默认 class,Job 保持 `Accepted=False/NoDefaultJobClass`。若存在多个全局默认值,
Job 保持 `Accepted=False/AmbiguousDefaultJobClass`,同时产生平台告警;不得模仿
StorageClass 选择最新创建对象,因为执行隔离与权限不应随创建时间变化。
解析后 Job status 保存:
```yaml
resolvedJobClass:
name: rootless
uid: 8aa4...
controllerName: execution.ayatori.ddupan.top/kubernetes
parametersRef:
group: execution.ayatori.ddupan.top
kind: KubernetesExecutionParameters
name: rootless
uid: c413...
```
Job 后续 reconcile 使用已解析引用,不能因 namespace 默认值或全局默认 class 改变而切换
adapter。若同名 class 被删除并重建,UID 不匹配,现存 Job 不得自动采用新对象。
## Status
```yaml
status:
observedGeneration: 1
conditions:
- type: Accepted
status: "True"
reason: Accepted
observedGeneration: 1
lastTransitionTime: ...
- type: Ready
status: "True"
reason: BackendReachable
observedGeneration: 1
lastTransitionTime: ...
```
### `Accepted`
表示 controller 已识别 controllerName,parametersRef 指向受支持且 schema 有效的对象,通用
resource policy 自洽。无效 class 使用 `Accepted=False` 和稳定 reason,例如:
- `UnsupportedController`;
- `InvalidParametersReference`;
- `ParametersNotFound`;
- `InvalidResourcePolicy`。
### `Ready`
表示该 class 当前具备接受新执行的基本条件。Kubernetes adapter 检查 RuntimeClass 等集群级
依赖;具体 namespace 中的 ServiceAccount 在 Job 调度时检查。OpenSandbox adapter 检查参数
引用、认证材料和后端健康端点。
`Ready=False` 阻止创建新的后端执行,但不改变已经开始 Job 的终态。Controller 仍必须尝试
Observe、Cancel 和 Delete 已存在执行,不能因 class 不 Ready 而停止清理。
Ready 是观测值,不是容量预留。容量不足、排队和并发配额属于调度系统,不通过 Ready 频繁
抖动。
## 生命周期与修改
- `controllerName` 和 `parametersRef` 不可变;切换后端必须创建新 class 名称。
- `allowedNamespaces` 与 resource policy 可以修改,只影响尚未接受的新 Job。
- adapter 参数对象允许更新 endpoint、Secret 引用和其他运维配置,以支持凭据轮换与故障切换。
- 参数更新不得使 adapter 为现存 Job 创建新的逻辑执行;Job 中已持久化的 execution reference
始终优先。
JobClass controller 添加保护 finalizer。删除 class 前必须确认不存在引用其 UID 的非终态
Job。终态 Job 已完成后端清理,不阻塞 class 删除;其 TTL 回收不再需要 class 后端配置。
参数对象删除保护由各 adapter controller 负责。在仍有 class 引用时,参数对象不得被无提示
删除。强制移除 finalizer 是管理员恢复操作,必须有 runbook 和审计记录。
## KubernetesExecutionParameters
这是 Kubernetes adapter 自己拥有的 cluster-scoped 管理员 API,不是 Job 用户 API。首版字段:
```yaml
spec:
serviceAccountName: string
runtimeClassName: string
scheduling:
nodeSelector: map[string]string
tolerations: []Toleration
podSecurityContext: PodSecurityContext
imagePullPolicy: Always | IfNotPresent | Never
```
首版原生 `batch/v1 Job` 与 Ayatori Job 位于同一 namespace,因此 Secret/ConfigMap、ResourceQuota、
NetworkPolicy、日志和 owner reference 都保持原生语义。普通调用者只拥有 Ayatori Job 权限,
不应拥有修改生成的 batch Job/Pod 的权限。
`serviceAccountName` 是每个允许 namespace 中预先提供的同名 ServiceAccount。缺失时 Job 保持
未调度并报告原因,不回退到 `default` ServiceAccount。
参数允许使用 Kubernetes 强类型的 Toleration 和 PodSecurityContext,因为这是明确属于
Kubernetes adapter 的管理员 API;这不构成向 Job API 透传 PodSpec。
## OpenSandboxExecutionParameters
这是 OpenSandbox adapter 自己拥有的 cluster-scoped 管理员 API。首版字段:
```yaml
spec:
endpoint: string
apiKeySecretRef:
namespace: string
name: string
key: string
poolRef: string
requestMapping: AdmissionOnly | Native
allowSecretEnv: bool
allowImageAuth: bool
```
- endpoint 必须为 HTTPS,Dev 显式允许的本地配置除外;不得包含认证信息。
- API key 只通过 namespaced Secret 引用,status/Event 不显示内容。
- poolRef 映射为 OpenSandbox 支持的 pool/profile 选择,不允许 Job 覆盖。
- `requestMapping=Native` 要求后端忠实接受 requests 与 limits;`AdmissionOnly` 表示 requests
只参与 Ayatori 准入,limits 映射为 OpenSandbox resourceLimits。
- Secret env 与 per-request image auth 都会使 adapter 读取 Kubernetes Secret 并把解析值发送
到 OpenSandbox API,必须由管理员分别显式启用。
OpenSandbox 参数不暴露任意 `extensions` map。未来确需使用某项 extension 时,将其提升为该
参数 CRD 中经过校验的命名字段。
## Condition 与 Job 状态机交互
Job 只有在以下条件同时满足时进入 `Accepted=True`:
- class 已按默认或显式名称解析;
- class UID 与已解析引用一致;
- class `Accepted=True`;
- namespace 符合 allowedNamespaces;
- Job resources 成功解析并处于允许范围;
- Job 使用的 Secret/ConfigMap 存在且可以按 optional 语义解析。
`JobClass Ready=False` 时,Job 保持 `Accepted=True`、`Scheduled=False`,等待后端恢复。
这样 class 配置合法性与当前可用性不会混成同一状态。
若 Job 已经 Scheduled,后续 class Ready 或 namespace label 变化不撤销执行。若 class 或参数
对象意外消失,controller 仍以 Job status 中的 controllerName、参数 UID 和 execution
references 尝试恢复;无法安全观察时进入非终态 `ResultUnknown`,不能切换 class 重跑。
## 测试矩阵
实现至少覆盖:
- 显式 class、namespace 默认和全局默认的优先级;
- 零个与多个全局默认 class;
- allowedNamespaces 允许、拒绝及接受后 label 变化;
- unsupported controllerName 和错误 parameters kind;
- parameters 不存在、稍后出现、UID 删除重建;
- resource defaults、min/max、request 大于 limit 和 quantity 边界;
- class policy 更新不改变已接受 Job 的 effective resources;
- class Ready=False 阻止新 Ensure,但不阻止现存执行 Observe/Cancel/Delete;
- Kubernetes ServiceAccount/runtime 配置缺失且不回退;
- OpenSandbox API key Secret 缺失、轮换及后端健康恢复;
- allowSecretEnv/allowImageAuth 拒绝不允许的 Job;
- class 删除被非终态 Job 阻止,终态清理后允许删除;
- 同名 class 或参数对象以新 UID 重建时不劫持现存 Job。
## 延后事项
- class 级并发和速率限制;
- Kueue LocalQueue/ClusterQueue 映射;
- capability-based 自动 class 选择;
- 多集群 Kubernetes executor;
- GPU、临时磁盘和其他扩展资源;
- workload identity 与 OpenSandbox Credential Vault;
- class 成本、优先级与抢占策略。
## 成熟实现参考
- [Kubernetes RuntimeClass](https://kubernetes.io/docs/concepts/containers/runtime-class/)
- [Kubernetes StorageClass](https://kubernetes.io/docs/concepts/storage/storage-classes/)
- [Gateway API GatewayClass](https://gateway-api.sigs.k8s.io/reference/api-types/gatewayclass/)
- [Kueue ResourceFlavor](https://kueue.sigs.k8s.io/docs/concepts/resource_flavor/)
+64 -8
View File
@@ -4,9 +4,11 @@
Git / CLI / Backstage
│
▼
Kubernetes API + CRD
kube-apiserver + etcd + CRD
API / state coordination plane
│
Ayatori controllers
Ayatori controller-manager
scheduling / lifecycle / recovery / GC
│
┌──────┼──────────────┐
│ │ │
@@ -19,6 +21,20 @@ Terraform OpenBao / DNS / KaaS
Ansible
```
Ayatori 复用 Kubernetes 的 API machinery,而不是 Kubernetes 的容器编排产品边界。
kube-apiserver 提供版本化对象、并发控制、list/watch、RBAC、admission 和审计;Ayatori
controller-manager 承担所有领域控制循环。Kubernetes workload 集群只是与 OpenSandbox、
Proxmox 等并列的 executor/backend,不默认等于运行 controller 的 management environment。
因此,领域 API 不得依赖“资源最终一定变成同集群原生对象”的假设。原生 Pod、Job、Service、
NetworkPolicy、namespace 共置与 owner reference 只有在 Kubernetes adapter 内才具有原生含义;
跨后端所需能力必须由领域模型显式定义。
内置 API 类型也按相同原则选择性复用。采用 `core/v1 Node` 作为计算节点 API 时,可以由
Ayatori Compute Agent 写入状态、由 Ayatori 自有调度 controller 消费;这不会引入 kubelet、
Pod 或 kube-scheduler。API contract、负责实现它的 controller/agent 和数据面是三个独立决策,
不得从其中一个自动推导另外两个。
## 控制面
Dev 与 Prod 使用独立的 Kubernetes API、数据库、身份和 controller 实例。两者可以
@@ -27,6 +43,34 @@ Dev 与 Prod 使用独立的 Kubernetes API、数据库、身份和 controller
Proxmox 作为稀缺物理基础设施可以共享,通过 pool、tag、token 和明确的资源范围区分
环境。其他后端尽量使用独立数据库、角色、地址池、DNS 空间与凭据。
## 进程内依赖边界
`cmd/main.go` 是唯一程序入口,只调用 `internal/bootstrap.Run` 并处理退出状态。
命令行参数、manager 创建、infra 与领域装配集中在 `internal/bootstrap`,
不在 `cmd` 平铺组件装配文件,也不为每个领域生成独立二进制。
Makefile 与 Dockerfile 均继续构建 `cmd/main.go`。
`Run` 只编排解析配置、创建 manager、显式装配组件、启动与退出清理。
`options.go` 组织配置和通用 flags;`manager.go` 处理 scheme、metrics、webhook、TLS 与探针;
`database.go`、`openbao.go` 各自维护组件参数及装配细节。新增组件不向 `Run` 堆叠参数和内部
条件分支,也不为此引入插件注册框架。组件启动失败时释放已装配资源,正常退出则先停止
manager worker,再释放连接。
基础设施能力属于整个 controller-manager,不因首个消费者是 Database 就归入该领域。
`internal/infra/openbao` 管理官方 SDK client 的 TLS 配置、Kubernetes 认证及 token 生命周期,
不依赖 Database 或其他产品领域。Bao client 默认禁用自动重试,写入结果不确定时由用例处理;
领域适配器不修改共享 client 的全局配置。Kubernetes 客户端、cache 和直连 reader 由 manager 管理;
启动入口负责装配与注入,不在领域适配器内重复创建客户端。
读写能力优先直接使用官方 `client.Reader`、`client.Client`、OpenBao KV API 等接口,
不为统一命名再包一层通用 reader/writer,也不引入全局注册中心。共享连接不表示扩大授权;
不同身份或权限边界仍由启动装配显式隔离。
领域按用例需要维护 repository 契约,其 adapter 负责 CR/领域对象映射及业务结果转换。
例如 Database 的七键凭据格式、UID 路径、禁止覆盖和不确定结果处理仍由 Database 维护;
它们不是公共 KV 存储的业务规则。Secret 管理凭据读取注入 `manager.GetAPIReader()`,
保持直连 API server、不缓存 Secret 内容的安全边界;资源写入复用 `manager.GetClient()`。
## 数据面
Ayatori 不承载或重新实现数据面。控制面故障只应阻止创建与变更,不应停止已有 VM、
@@ -34,16 +78,16 @@ Ayatori 不承载或重新实现数据面。控制面故障只应阻止创建与
## 资源分层
平台提供正交产品能力,例如:
平台只为已经验证的管理缺口提供正交产品能力。当前优先资源为:
- `Job`、`Sandbox`、`ManualTask`
- `VirtualMachine`
- `LoadBalancer`
- `Database`
- `Bucket`
- `DNSRecord`
- `Credential`
- `KubernetesCluster`
- `VirtualMachine`
`Run`/当前实验性的 `Job`、`ManualTask` 等可以作为控制面执行原语,但不是因为底层能运行 OCI
image 就自动成为面向使用者的计算产品。`DNSRecord`、`Credential`、`KubernetesCluster` 等只在
出现独立生命周期和真实消费者后加入;尤其 KaaS 不是预定终点。
只有具备独立领域生命周期的能力才应成为高阶资源。应用本身通过 GitOps 组合上述资源,
重复组合可通过模板或 Composition 表达,而不是扩展中央 Application API。
@@ -56,5 +100,17 @@ Ayatori 不承载或重新实现数据面。控制面故障只应阻止创建与
2. 通过固定版本的 Terraform module 或 Ansible playbook 执行。
3. 仅在必要时使用 GitOps bridge。
Proxmox 是已知例外:其远程 API 不能覆盖所需的完整 VM 生命周期。VirtualMachine adapter 可以
按操作能力选择 Proxmox API、部署在节点上的受限强类型 Agent/CLI,或生成 `ManualTask`。Agent
必须提供版本化操作、幂等查询、operation ID 与审计,不能暴露任意 shell,也不能把 CLI 输出
直接当作长期稳定协议。
Controller 无论采用哪种执行方式,都必须提供一致的 ownership、conditions、删除语义、
错误分类和恢复行为。
## API Server 边界
首选 kube-apiserver + CRD,持续复用其成熟的 watch、RBAC、版本化存储和 API 生态。
generic-apiserver 或聚合 API Server 不会减少领域 controller 的数量,只会把资源服务端、
兼容性和存储迁移责任转移给 Ayatori。只有 CRD 的限制已经形成可复现、不可通过合理领域建模
解决的阻碍时,才重新评估自建 API Server。
+5
View File
@@ -26,6 +26,11 @@ cloud-init 设备、bridge/VLAN 映射和默认 placement 由平台维护。
## 生命周期基线
新增资源、绑定、回收或恢复语义前,先引用 Kubernetes 官方对应资源设计与成熟 controller
模式,说明采用部分及有意偏离的原因。不要仅沿用字段名称而忽略生命周期与权限边界。
例如 Database 借鉴 PV/PVC 的资源与申请分离、排他绑定和 Retain,但不引入 CSI 协议、
存储调度或额外 registry;见 [ADR-0009](../decisions/0009-database-resource-and-claim.md)。
所有受管资源必须定义:
- `observedGeneration`
+13
View File
@@ -1,5 +1,18 @@
# 环境与发布
## CI 验证入口
Verify 工作流在 PR 上执行全量测试、lint 和 Database 集成测试;合并到 main 后不通过 push
事件重复运行。需要排障或验证直接推送的紧急修复时,可通过 workflow_dispatch 手动运行。
此约定不减少检查项目,也不修改分支保护设置;常规变更必须经过 PR,直接推送 main 不会自动验证。
Gitea 的 PR 工作流验证分支 head,而不是合并预览提交,见
[官方事件说明](https://docs.gitea.com/usage/actions/faq/)。合并前必须确认最新 head 检查通过,
且与当前 main 合并不会引入未经验证的内容组合;基线有实质变化时先更新分支并重验。
只改变基线引用且目标文件树不变时,不需要为了合并提交的 SHA 不同重复全量验证。
## 环境与制品晋级
Ayatori 首先建立 Dev。首个产品能力完成开发并达到可发布状态前,Prod 不实际存在;此时
没有生产制品需要承载,提前维护第二套环境没有收益。
+264
View File
@@ -0,0 +1,264 @@
# Database 模块
Database 是 Ayatori 首批实际产品领域之一。当前已包含三资源 API、分层绑定与 Instance 原生
管理能力观测;尚未完成 Database 供应/导入、Tenant 凭据交付与资源回收链路。
## 当前设计(2026-09-24)
采用 Instance → Database → Tenant 的资源与申请模型;Database 独立存在,支持显式导入、
Retain 后人工重新绑定与资源侧 Delete。撤销 PostgreSQL ownership registry 及任意 status
丢失自动恢复所有权的要求。未知同名资源或创建结果不确定时,清楚报告 Conflict 并人工处理。
依据 [ADR-0009](../decisions/0009-database-resource-and-claim.md),当前合同见
[系统规格](specification.md)。下面的迁移来源与已存在代码不反向约束新设计。
registry adapter、专属迁移/测试及 Instance 的 registry 判定现已撤除;Instance 根据完整管理
能力观察直接判定 Ready。Database 资源与绑定已接入,导入、角色/凭据供应及回收仍未完成。wiki 同步位置见
`homelab-wiki/services/postgresql-tenant-operator.md`,跨仓库发布状态由 wiki 的同步记录维护。
## 来源基线
完整设计合同及首批领域模型与测试提取自原 PostgreSQL Tenant Operator:
- 仓库:`git.ddupan.top/panxiao81/postgresql-tenant-operator`
- source commit:`dae546e58efa1be81e930861c87f7fb13bb12113`
- 原路径:`internal/domain/instance/`、`docs/domain-instance.md`
- 迁移日期:2026-09-20
本目录迁入该基线的 specification、architecture、API、领域、部署、安全、开发、迁移与运维
文档,最初只进行 Ayatori 产品归属、API group、目录和链接适配;2026-09-24 经维护者
批准的资源/申请分离修订明确替代 registry、自动恢复与原 Retain 合同,其余适用的安全约束保留。
迁移只使用该 commit 中已提交的文件。源仓库
`feature/instance-extension-observations` 工作树中的 `instance.go` 修改与
`instance_extensions_test.go` 未进入本切片。
代码被移动到 Ayatori 的 `internal/database/domain/instance`,测试 import 和文档链接相应更新;
首个后续切片按已批准合同增加 Instance extension observation:观测与当前 target 绑定,进入重新
验证或删除时失效,且支持判定不授权 Tenant provisioning。已有 Ready 切片曾按原合同加入
registry 准备决策;这一依赖现已从代码移除,不能把旧运行链路接回模型。
各层验证边界见 [Instance 领域规格](domain-instance.md)。
## 边界
- 领域层不依赖 Kubernetes types、数据库 driver 或凭据 provider。
- CredentialReference 只携带管理 Secret 的名称与字段映射,不包含 Secret 内容或 OpenBao path。
- Instance checkpoint 不是外部事实;实际能力必须由 application/adapter 观察后交给领域对象判断。
- Instance 观察只检查供应前置条件,不单独授予 Tenant 所有权或外部写入权限;凭据准备
另行校验双向绑定和保护,尚不表示完整 Database API 已经可用。
## 管理凭据与连接切片
`application.InstanceService` 适配自原项目固定基线
[`internal/instance/service.go`](https://git.ddupan.top/panxiao81/postgresql-tenant-operator/src/commit/dae546e58efa1be81e930861c87f7fb13bb12113/internal/instance/service.go),
保留 CredentialReader、Connector、Database 的装配边界及串行操作/释放规则。
具体连接池完全由 pgxpool v5.11.0 提供,PostgreSQL adapter 不拥有凭据缓存、轮换流程或任意观测回调。
相对源基线有两项按已批准合同作出的必要修改:管理凭据从固定 controller namespace 的
Kubernetes Secret 直接读取;每轮比较有效用户名、密码,检测到变化即关闭旧连接并重新装配。
Secret metadata 和无关字段变化不重建连接。观测后再次读取 Secret,中途有效值变化则丢弃结果,
不把旧连接的成功作为新凭据有效的证据;这不构成跨 Kubernetes/PostgreSQL 的原子事务。
Instance UID、endpoint 或凭据引用变化也会释放旧连接;Forget/Close 只释放本地资源。
`ObserveMetadata` 读取服务器版本和可用扩展,`ObserveVersion` 是版本读取便捷入口;两者
不会填充管理检查,不能产生 Ready。`ObserveManagement` 使用同一凭据/连接边界读取完整
原生管理检查,返回绑定当前 target 的 `InstanceObservation`。controller 的资源呈现适配器
使用 resourceVersion 拒绝过期写入。应用层沿用串行处理,不引入新的调度框架。
运行 `make test-database-integration` 验证真实 API server + 一次性 PostgreSQL;fixture 不接受外部
DSN,镜像固定摘要,使用随机本机回环端口并在退出时删除测试容器。覆盖缺失/错误凭据、RBAC、
namespace 边界、有效值轮换、metadata 无关变化、中途轮换、重建/重试、并发读取、Forget/Close
与 TLS DNS/IP SAN、错误 CA/主机名和禁止明文降级。CI 使用 Pod runner 执行,由 runner 提供
可用的 Docker,workflow 只做预检、不自行启动 daemon;不依赖 VM。普通 lint 之外还检查
integration 标签代码。领域单测、真实 API 行为与真实 PostgreSQL 行为分别验收,不以本切片
替代整个 Instance controller 的集成验收。
## 服务器 metadata 与扩展观测
SQL adapter 通过一条只读语句读取 `pg_catalog.current_setting('server_version')` 和
`pg_catalog.pg_available_extensions`,避免从已安装列表推断可用列表,且不依赖可修改的
`search_path`。查询失败丢弃整份结果;成功返回空列表与尚未观察严格区分。
参考 PostgreSQL 的 [pg_available_extensions](https://www.postgresql.org/docs/18/view-pg-available-extensions.html)
与 [CREATE EXTENSION](https://www.postgresql.org/docs/18/sql-createextension.html) 合同:可用列表
表示服务器提供的扩展,不证明管理账号有安装权限,也不保证依赖和其他安装前提满足。
`InstanceService` 保留原有 CredentialReader → Connector → Database 边界,复用同一个
凭据读取、连接刷新和串行释放流程,不新增连接池封装或任意查询回调。每次重新查询 metadata,
并在 Secret 有效值回读一致后生成不可变的 `InstanceObservation`,绑定本次 target(含当前
generation),不绑定建池时的旧 target。结果不包含凭据,扩展集合不与 driver 的可变 slice 共享。
凭据中途变化、读取失败或查询失败时,返回零值观察并释放连接,不复用旧的扩展列表。
调用方可将 `Target()` 与 `Extensions()` 交给 Instance 的 `ObserveExtensions`;应用调用链
仍负责同轮次使用,不能持久化或跨轮缓存这份证据。metadata 读取不安装扩展、不初始化 registry、
不设置 Ready,也不授予 Tenant 写权限。管理权限检查使用下面的独立入口。
真实 API server + PostgreSQL 测试验证未安装扩展可被观察、名称保持大小写、search_path 遮蔽
不改变查询来源、低权限账号读取、权限撤回失败与恢复、Secret 中途变化丢弃扩展结果。
单元测试补充成功空列表、查询附带部分数据时丢弃、结果与可变 slice 隔离、每轮重新读取和
generation 变化时的目标绑定;原凭据/TLS/超时/并发测试沿同一 metadata 路径继续运行。
## Registry 撤除
原 `adapter/postgresql/registry`、SQL 迁移、registry 专属集成测试及 tern 依赖已删除;
未提交的 inspection 实验也已撤除。来源仍可在 Git 历史追溯,没有删除外部 PostgreSQL 对象。
Instance 不再具有 InitializingRegistry 阶段、RegistryState、准备决策或回读方法;
首次完整管理能力观察即可完成验证,重验失败则撤销本轮就绪证据。
保留凭据读取与连接刷新、TLS、metadata/扩展观察及其真实后端测试。领域测试覆盖每项能力
在初次验证和 Ready 重验时失败、依赖恢复、重启后重新取证、错误目标/阶段及删除保护。
这不等于三资源供应、交付和回收已实现。
## Instance 原生管理观测
2026-09-25 维护者确认先使用原生非 superuser 方案,不引入 SECURITY DEFINER 接口。
`InspectManagement` 通过同一条只读语句读取当前执行角色的 `CREATEROLE`、`CREATEDB`、
superuser 属性、服务器可写状态、版本和扩展列表。不创建探针数据库/角色,不初始化 schema。
只有非 superuser、具备两项原生属性且当前服务器/会话可写时,基础管理能力才通过。
角色属性不可从继承成员关系推导;具体已有资源仍须检查 owner、membership 与授权范围。
权限依据和真实测试对应:
- role:当前角色具有 CREATEROLE,可创建普通登录角色;
- database:当前角色具有 CREATEDB,且会话非只读、服务器不在 recovery;
- grant:使用自己新建角色的管理权限,显式建立 SET membership,再以 owner 管理数据库 ACL;
- extension:新建数据库 owner 可安装 trusted 扩展;可用列表不是安装授权,非 trusted 或
其他前提不满足的扩展仍可能失败,必须逐请求执行和回读。导入不继承此动态供应授权。
参考 PostgreSQL 官方 [CREATE ROLE](https://www.postgresql.org/docs/18/sql-createrole.html)、
[CREATE DATABASE](https://www.postgresql.org/docs/18/sql-createdatabase.html) 与
[CREATE EXTENSION](https://www.postgresql.org/docs/18/sql-createextension.html)。这些检查是基础
能力观察,不是未来操作必然成功的保证;权限、容量、连接数等仍可能在执行时变化。
`InstanceReconciliation` 协调 finalizer、观察、领域判定和删除引用检查,controller 只连接
事件、用例、状态呈现与重试。每轮重建无证据的领域对象;旧 Ready 不授权新一轮操作。
失败清除当前版本结果并撤销 Ready;CR 在观察期间被修改则拒绝旧结果,下一轮重新读取。
连接/权限变化由 Secret watch 和 30 秒重查驱动,单轮 IO 最长 15 秒;不持续写入相同状态。
manager 通过 `--database-secret-namespace`(默认 `POD_NAMESPACE`)启用 Instance 观测;
为空时不启用。本地运行需显式提供该参数。Deployment 使用 downward API 获取自身 namespace;
Secret 的 get/list/watch 权限由该 namespace 的 Role 单独授予,不放入 ClusterRole。
watch 使用 controller-runtime 的 metadata-only cache,读取有效凭据仍直连 API server。
TLS 使用既有 endpoint 合同,公开 CA bundle 可由 `--database-root-cert` 指定;不会自动挂载
生产证书或创建管理 Secret。manager worker 停止后统一关闭 pgxpool。
Instance 删除首先释放本地连接并撤销 Ready。任何引用它的 Database(含 Released、删除中)
或动态 Tenant 申请都会阻止 finalizer 解除;列表查询失败也等待。仅在引用全部解除后移除
`database.ayatori.ddupan.top/instance-protection`,不删除 PostgreSQL、账号或凭据。
引用查询与删除不是跨对象事务;后续供应仍必须拒绝已删除/删除中的 Instance。
验收使用真实 PostgreSQL + API server:原生管理账号实际建库、owner 授权、trusted 扩展
安装/回读,拒绝非 trusted 扩展;权限撤回/恢复、只读会话、superuser 拒绝和中途轮换。
实际 manager 在生成的资源 RBAC 和 namespaced Secret Role 下验证缺失 Secret 后出现、
轮换、删除、跨 namespace 拒绝和 watch。API 测试另覆盖写入版本冲突、幂等、新 reconciler
恢复与引用删除保护。完整 DBaaS 仍需供应/导入、OpenBao/ESO、Retain/Delete 集成验收。
## 应用凭据存储切片
`adapter/openbao` 使用官方 Go SDK `api/v2 v2.7.0` 的 KV v2 API,只有创建和读取,
不维护 registry、不覆盖已有密码。动态路径由固定前缀与 Database UID 组成;所有访问都校验
配置前缀,已有导入位置也不能绕过 controller 的凭据权限范围。
创建使用 CAS=0,随后回读七键和版本 1;已有值或软删除历史报冲突。关闭 SDK 自动重试,
写入响应丢失、回读失败或内容变化均返回不确定结果,上层不得生成第二份密码或自动认领。
`Read` 只适用于调用方已确认关联的路径,读取成功本身不是管理权证据。错误不传播 SDK
响应体;内存凭据的普通格式化及 JSON 输出均脱敏,明确的 `SecretData` 才返回明文七键。
依据官方 [KV v2 CAS 合同](https://github.com/openbao/openbao/blob/main/internal/builtin/logical/kv/path_data.go)
与 [Go SDK](https://github.com/openbao/openbao/tree/main/api)。`make test-database-integration`
现包含独立 OpenBao dev 容器,固定摘要、随机回环端口、无持久卷,不接受外部地址。
真实后端覆盖创建/回读、并发唯一创建、重建适配器读取、软删除冲突、固定前缀 token
拒绝管理路径,以及成功写入后丢失响应;HTTP 故障测试补充不重试和错误脱敏。
认证会话和凭据准备用例已接入 manager,但默认不启用外部写入。
Database 已有 `status.credentialRef` 和 `status.credentialVersion` 的字段与 CEL 校验:
固定位置、只在创建并回读成功后确认版本,二者写入后不可清空或修改。
`ReadConfirmed` 按已确认版本检查最新 KV 值,删除或版本漂移报 Conflict,不读取旧版本掩盖变化。
测试 token 只用于临时 fixture,不是生产静态 token 配置接口。
### 凭据准备闭环
配置认证后,显式设置 `--database-credential-mount` 启用准备;路径默认
`applications/<Database UID>`,前缀由 `--database-credential-prefix` 配置。
`CredentialPreparation` 用例检查 Provision 来源、双向绑定 UID、申请目标、finalizer、
删除/Released 状态及当前 Instance Ready;导入资源不执行本流程。
顺序为固定位置 → 确认后端尚无凭据 → 保存 CreationStarted → 创建并回读 → 保存确认版本。
`CredentialReconciler` 只负责 Database/Tenant/Instance watch 和 30 秒依赖重查;
Kubernetes repository 负责直接读取及有 resourceVersion 保护的状态更新。
外部操作前后回查目标:Database 必须仍是同一 UID/resourceVersion,Tenant/Instance 必须
保持绑定、spec generation、删除状态、finalizer 保护与有效 Ready;无关 Conditions 刷新
不构成目标变化。不存在跨 Kubernetes/OpenBao 原子事务:中途发生相关变化时停止确认,
留下可观察状态,交给后续协调或人工核实,不盲目重试过期的确认写入。
`CredentialsReady=False/CreationStarted` 是正在进行而未确认的诊断,不是成功证明。
任何新一轮协调遇到该状态且没有确认版本,都转为 Conflict;即使进程在发出请求前退出
也采用这一保守边界。明确的认证/权限拒绝可等待恢复;响应丢失、回读失败、确认保存失败
及未确认的已有值均不会自动认领或重新生成密码。Conflict 保留首次原因,普通依赖错误
保留已确认版本;配置变化停止在原位置,不搬迁或覆盖。并行实例中途撞上已开始的创建
同样可能保守地要求人工处理,不承诺无损接续;多副本部署应启用既有 leader election。
成功只设置 `CredentialsReady=True`,Database Ready 仍为 False/ProvisioningIncomplete,
Tenant 仍未完成交付。本切片没有 PostgreSQL role/database 创建、扩展安装、ESO 投射、
Retain 释放或 Delete 清理,也不会解除 finalizer。不要作为完整 DBaaS 部署。
单元测试穷举前置条件;真实 API server + 隔离 Bao 验证创建、状态确认、幂等、重启、并发、
依赖恢复、固定位置、不确定结果、确认保存失败和删除边界;实际 manager 验证 watch 驱动
及重启。该 fixture 只声明 Instance 前置 Ready,真实 PostgreSQL 管理能力由既有 Instance
集成测试覆盖,不把凭据准备验收当成实际建库或应用登录验收。
## OpenBao Kubernetes 认证会话
公共 `internal/infra/openbao.KubernetesSession` 复用官方 Kubernetes auth helper 和 `LifetimeWatcher`
(均为 v2.7.0)。认证直接注入 `manager.GetClient()`,与 reconcile 共用已装配的 Kubernetes
client,不从配置另建客户端。标准 `--kubeconfig` /
`KUBECONFIG` 支持 systemd 或其他集群外运行方式,集群内使用 in-cluster 配置,不要求存在 Pod。
Kubernetes 身份的签发和更新由部署管理及 client-go 的认证机制负责,不另建 kubeconfig 读取器。
每次登录前,通过该 client 的 `SubResource("token").Create` 调用固定 namespace/name 的
ServiceAccount TokenRequest;写入直连 API server,不读取 cache 或要求额外的 SA get 权限。申请
audience 匹配 OpenBao role、期望有效期 600 秒的短期 JWT;检查返回值非空且未过期,再交给
官方 Kubernetes auth helper。JWT 不缓存,不读取投射文件,也不回退静态 OpenBao token;
实际 JWT 有效期由 API server 决定。RBAC 拒绝或 TokenRequest 失败时不会继续 Bao 登录。
OpenBao 登录结果必须包含有效 token 和有限 TTL。
续期、到期阈值与等待时间由 SDK 管理;可续期 token 的续期失败就撤下本地 token,不可续期
token 由 SDK 监测剩余寿命。会话结束后最多每 5 秒重新登录一次,并重新申请 Kubernetes JWT。
`Ready()` 仅表示当前 lease 正受 SDK 管理,不授权任何 Database 写入,也不能保证下一次请求
必然成功。认证/续期响应不写日志、不返回给调用方;后端操作仍独立检查并返回脱敏错误。
`Start(ctx)` 退出时清空 client token 并等待续期 goroutine 结束。SDK Stop 不取消已经发出的
续期 HTTP 请求,因此专用 client 的请求期限固定为 15 秒;不增加新连接池或自己的续期算法。
同一会话拒绝并发 Start。manager 使用 Runnable 管理生命周期,并增加 `openbao-auth` readiness
检查;认证故障不影响 liveness。`--openbao-address` 为空时不启用,不自动修改生产 auth/RBAC。
HTTPS 和显式 CA/系统信任根不可通过 BAO 环境变量降级,参数见 [部署合同](deployment.md)。
依据官方 [Kubernetes auth](https://openbao.org/docs/auth/kubernetes/) 与
[token 生命周期](https://openbao.org/docs/concepts/auth/)。真实测试使用 envtest 签发 SA token,
OpenBao 通过专用 reviewer 调用真实 TokenReview;集群外受限 kubeconfig 启动实际 manager,
验证共享 client 申请 JWT、短 TTL 续期、RBAC 撤回/恢复与重新登录、跨 namespace/其他 SA 拒绝、
错误 OpenBao audience 拒绝以及凭据访问恢复。临时 TokenReview 入口仅允许对应 POST,
两段连接均验证 TLS;其 Docker bridge 入口仅为隔离测试,不修改生产 OpenBao 或 Kubernetes。
单元测试补充 TokenRequest 失败/空 token 无回退、重新申请 JWT、无期限 lease 拒绝、
并发生命周期、退出清理及 manager 显式参数不受 BAO 环境身份覆盖。
## 公共基础设施与领域适配
Bao client/TLS 与认证生命周期已移至 `internal/infra/openbao`,与任何产品领域无关。
Kubernetes 读写客户端由 manager 管理,Secret 凭据适配器只接收直连的 `client.Reader`。
`adapter/openbao.Credentials` 仍属于 Database:它直接使用官方 KV v2 API,实现七键凭据、
UID 路径、CAS=0 与回读确认的领域合同,不把这些规则推广为公共存储语义。
后续供应用例需要的 repository 接口由领域侧按实际操作定义,不提前增加通用仓储抽象。
分层约定见[总体架构](../architecture/overview.md#进程内依赖边界)。
认证单元测试归公共 infra;真实认证与 Database 凭据读写的组合测试仍在 Database adapter。
Database 集成测试和 lint 入口同时覆盖 `internal/infra/...`,避免拆包导致 CI 漏测。
## 设计入口
- [系统规格](specification.md):规范性行为与验收标准;
- [架构](architecture.md)与[API 合同](api-reference.md);
- [领域模型](domain-model.md)与[Instance 领域规格](domain-instance.md);
- [安全](security.md)、[迁移](migration.md)与[运维](operations.md);
- [部署合同](deployment.md)与[开发测试合同](development.md)。
部署和开发文档描述目标合同,其中旧仓库专属的 Make target、脚手架版本和目录尚未接入 Ayatori;
在对应实现切片完成前,不应把其中命令理解为当前仓库已经可执行的入口。
+213
View File
@@ -0,0 +1,213 @@
# v1alpha1 API 合同
| 项目 | 内容 |
| --- | --- |
| 状态 | API schema 与绑定 controller 已实现;供应、交付与删除清理未接入 |
| API group/version | `database.ayatori.ddupan.top/v1alpha1` |
| 最后更新 | 2026-09-27 |
以 [系统规格](specification.md) 与
[ADR-0009](../decisions/0009-database-resource-and-claim.md) 为准。类型与生成的 CRD 已纳入源码,
尚未发布为可用 DBaaS。示例可进入绑定协调,但不代表创建对象后会供应数据库或交付凭据。
## 当前 API 切片
Go 类型位于 `api/database/v1alpha1`,CRD 随 `config/crd` 发布;manager 已注册 Scheme 和
`internal/database/controller` 的绑定 controller。以下字段是本切片的具体实现:
| 资源 | 字段 | 含义 |
| --- | --- | --- |
| Database | `spec.instanceRef.name` | 所属集群级 Instance |
| Database | `spec.database`、`spec.loginRole` | 实际数据库与唯一登录 owner,必填 |
| Database | `spec.source` | 必填 `Provision` 或 `Import`,不隐式认领 |
| Database | `spec.credentialRef.mount/path` | Import 必填的已有 KV v2 凭据位置;Provision 禁止指定 |
| Database | `spec.reclaimPolicy` | Retain 默认或 Delete |
| Database | `spec.tenantRef.namespace/name/uid` | controller 写入的完整绑定身份,不是允许名单 |
| Database | `status.instanceUID` | 观察时的 Instance 身份 |
| Database | `status.credentialRef.mount/path` | 首次写入前固定的 KV v2 位置,不随部署配置迁移 |
| Database | `status.credentialVersion` | 创建并回读成功后确认的正整数版本;省略表示未确认 |
| Tenant | `spec.provision.instanceRef.name` | 动态申请来源,与 `spec.databaseRef` 互斥且必须二选一 |
| Tenant | `spec.provision.database/loginRole` | 可省略,语义默认值由 controller 解析,不由 CRD 推导 |
| Tenant | `spec.databaseRef.name` | 显式申请已有 Database,不额外指定 Instance |
| Tenant | `spec.extensions`、`spec.secretName` | 扩展集合与同 namespace 的交付目标 |
| Tenant | `status.databaseRef.name/uid` | 资源侧绑定成功后写入 |
| Tenant | `status.secretName`、`status.credentialURL` | 交付观察,不包含密码或认证信息 |
三资源均有 status subresource、observedGeneration 和按 type 唯一的 Conditions。
Instance phase 沿用已批准枚举;Database/Tenant phase 暂不冻结供应子阶段枚举。
`credentialRef.path` 是 mount 内逻辑路径,不包含 KV v2 的 `data/` 前缀。
其部署允许范围、实际凭据读取和 URL 安全构造仍由后续 adapter/controller 验证。
凭据位置与确认版本一旦写入便不可更改或移除;确认版本必须有对应位置。Conditions 描述
当前可用性,不替代确认记录,也不能因读取暂时失败而清空记录。上述 schema 已有真实 API
server 校验;独立的凭据准备用例在显式启用后填写这些字段,绑定 controller 不负责外部写入。
未确认的已有值必须报 Conflict,不能用读取成功补记版本;已确认版本的恢复读取检查最新
KV 版本,删除或版本漂移均需人工处理,不回退旧版本或生成替代密码。第一版不提供轮换入口。
`CredentialsReady` 条件只描述凭据准备结果;它不授权实际数据库交付。
`CreationStarted` 且无确认版本表示创建未完成确认,重入时停在 Conflict;不尝试推断
进程中断前请求是否发出。完整执行和测试边界见[凭据准备闭环](README.md#凭据准备闭环)。
示例:[Instance](../../config/samples/database_v1alpha1_postgresqlinstance.yaml)、
[导入 Database](../../config/samples/database_v1alpha1_postgresqldatabase.yaml)、
[动态/已有资源申请](../../config/samples/database_v1alpha1_postgresqltenant.yaml)。
当前 schema 验证名称、端口、IP、TLS 枚举、申请互斥、导入凭据要求和绑定 UID 完整性。
API 接受两个 Tenant 引用同一 Database 不表示允许双重绑定;排他绑定由 controller 协调。
绑定 controller 解析动态 database/loginRole 的 Tenant 名称默认值;动态资源 CR 名称为
`tenant-<Tenant UID>`,首次创建即包含资源侧绑定与 finalizer,不带 Tenant ownerReference。
已有资源必须有当前版本 Ready 观察、匹配的 Instance UID,并处于未绑定的 Available 状态。
同一 Tenant 的资源侧记录已写入时,允许回读后补齐申请侧,不重新争抢资源。
Tenant 进入 `status.phase=Binding` 后由 CEL 固定申请目标;Database 有实例身份观察、凭据位置或
绑定后固定实际 database、loginRole、来源和凭据引用,回收策略仍可修改。
读取绑定判断使用 APIReader,写入依靠 resourceVersion;watch/cache 负责触发协调。
绑定顺序由 application service 协调,纯资格规则在领域层;Kubernetes adapter 负责快照
映射、finalizer 和状态呈现。呈现前若资源版本已变化,返回冲突供下一轮重读,不覆盖其他修改。
双向记录完成后 Tenant 为 Bound,Ready=False/BindingComplete,明确尚未供应或交付。
生成的 manager ClusterRole 授予资源读写,不包含 Secret 读取;Instance 观测的管理 Secret
权限由固定 namespace 的独立 Role 授予。Instance controller 已接入原生管理观察与引用删除
保护,启用方式及 Ready 边界见 [模块说明](README.md#instance-原生管理观测)。
当前有 Tenant/Database finalizer 保护,但**删除清理尚未实现**:Tenant 删除报告
Ready=False/DeletionPending 并保留绑定与 finalizer,Database 的保护也不会被自动移除。
还未实现删除流程开始后的回收策略固定、Retain 释放、Released 重新开放、外部清理、
扩展只追加、Secret 默认名称解析与凭据交付。在后续清理协议和真实后端验收完成前,
不能作为可用 DBaaS 部署,也不能通过强行移除 finalizer 把它视为已完成清理。
## 通用约定
- Instance 与 Database 是 cluster-scoped;Tenant 是 namespaced。
- Tenant 按名称引用 Database,不提供 Database namespace;Database 绑定记录包含
Tenant namespace/name/UID。字段见当前 API 切片;绑定采用下述资源侧先写顺序。
- 每类资源提供唯一的 Ready Condition、observedGeneration;phase 用于进度展示,
不能单独作为写权限或所有权证明。
- 引用必须区分定位名称与已绑定 UID;同名新对象不继承绑定。
- controller 管理 status;资源侧管理与回收权限不得随 Tenant editor 权限自动授予。
- 固定默认值使用 CRD defaulting,跨字段/不可变校验使用 CEL 或 controller;
并发更新使用 resourceVersion,不增加 mutating webhook 或跨系统事务。
- database/role identifier 继续匹配 `^[a-z][a-z0-9_]{0,62}$`。
- 动态申请的 database/loginRole 省略时继续以 Tenant metadata.name 为语义默认值;
显式导入资源使用实际目标,不从新 Tenant 名称重新推导。最终互斥字段须经 API 评审。
- 投射仍位于 Tenant namespace,ExternalSecret 默认命名沿用
`<instanceRef>-<metadata.name>-postgresql`;目标 Secret 可由申请指定,省略时同名。
有效 Instance 名称与 Tenant 名称合计不超过 241 字符;完整名称须满足 API 名称校验。
- 任何 spec/status/错误不得出现密码、Token 或完整秘密响应。
## PostgreSQLInstance
保留管理入口字段:
| JSON path | 类型 | 必填/默认 | 合同 |
| --- | --- | --- | --- |
| `spec.endpoint.host` | string | 必填 | PostgreSQL DNS 名;必须被服务端证书 DNS SAN 覆盖 |
| `spec.endpoint.hostaddr` | string | 必填 | 单个 IPv4/IPv6;必须被服务端证书 IP SAN 覆盖 |
| `spec.endpoint.port` | int32 | `5432` | 1–65535 |
| `spec.endpoint.database` | string | `postgres` | 管理连接 database;合法 PostgreSQL identifier |
| `spec.endpoint.sslMode` | enum | `verify-full` | `disable`、`require`、`verify-ca`、`verify-full` |
| `spec.adminCredentialRef.name` | string | 必填 | controller namespace 内的管理 Secret 名称 |
| `spec.adminCredentialRef.usernameKey` | string | `username` | Secret data 中的键名 |
| `spec.adminCredentialRef.passwordKey` | string | `password` | Secret data 中的键名 |
管理 Secret 固定在 controller namespace,不接受 namespace 或 Bao path。
管理员维护其 ExternalSecret,controller 只读;有效用户名/密码变化时重验连接。
endpoint 变化使旧观察失效,不迁移旧服务器的数据,不自动接管旧 UID 的资源。
Status 保留 observedGeneration、postgresqlVersion 和 conditions;
phase 为 Pending、Validating、Ready、Deleting,移除 InitializingRegistry。
Ready 要求连接、metadata 与所需管理权限,不要求 registry。
实例实际可用扩展来自查询,不提供 allowedExtensions 配置。
开始受管时保存 finalizer;删除时阻止新供应,并检查 Database(含 Released)及未绑定
Tenant 引用。无引用才解除,不级联删除资源;查询失败不能视为无引用。
## Database 资源(工作 Kind:PostgreSQLDatabase)
v1alpha1 以一个 database、一个兼任 owner 的 login role 及其应用凭据作为 Database 的
生命周期边界;不提供多账号字段或独立 Role/Credential CRD。多账号需求留待后续 API 版本。
此决定不改变导入只读验证和显式管理授权的要求。
Database 是平台管理的集群级资源,不归属于应用 namespace,也不需要资源专用 namespace。
普通申请者通过 Tenant 申请使用,不能自行修改 Database 回收策略或将 Released 资源重新开放;
这些资源管理操作由平台管理员授权。controller 的绑定协调权限与用户申请权限分别配置。
以下是行为合同,具体 schema 见当前 API 切片与生成的 CRD;后端行为尚未实现:
| 内容 | 合同 |
| --- | --- |
| `instanceRef` | Database 自身必填;定位来源并记录绑定的 Instance UID,不依赖 Tenant 补齐 |
| 外部目标 | 实际 database 名称及已确认的管理范围;操作开始后不可隐式改目标 |
| 来源 | 区分动态供应与管理员显式导入;不能从同名存在推断导入授权 |
| 绑定 | 至多一个 Tenant,含 namespace/name/UID;Released 保留旧身份;无允许绑定名单 |
| 回收策略 | 资源侧 Retain(默认)或显式 Delete;普通 Tenant editor 不得扩大授权 |
| 观察与进度 | 实际目标、当前阶段、条件和安全诊断,不保存秘密 |
概念生命周期包含供应/验证、可绑定、已绑定、Released 和删除;最终 phase 枚举待协议评审。
Released 不自动变成可绑定。Database 不以 Tenant 为 GC owner;使用中的资源受删除保护。
导入的初始检查只读;存在不等于 Ready。未绑定且可用的资源允许 Tenant 显式申请,
不要求管理员逐 Tenant 授权;已占用或 Released 的资源不能直接绑定。
默认保留不隐含密码、owner、授权或删除的变更许可。
## PostgreSQLTenant
保留用户申请与交付职责:
- 动态申请描述 Instance、所需数据库/登录角色、扩展与目标 Secret;
或显式引用管理员已登记的 Database。后者从 Database 获取 Instance,不重复指定来源;
两条路径互斥,具体 schema 在 API 切片固定。
- 绑定前固定有效需求,绑定/开始供应后不能通过改引用或名称迁移资源。
- extensions 成功后只允许追加,不自动卸载。
- status 展示绑定 Database 身份、Ready、交付 Secret 引用及不含认证信息的 OpenBao URL。
- Tenant 删除释放申请,按照 Database 的回收策略处理,不独立持有最终删除授权。
旧 Tenant `spec.deletionPolicy` 不再作为最终资源回收策略;旧字段表、供应 phase 枚举及
仅按 Tenant namespace/name 派生凭据路径的规则不再是实现合同。已有代码没有兼容负担,
不保留两套相互覆盖的策略字段。
## 已确认的绑定顺序
1. 动态申请按 Tenant UID 确定 Database 名称并创建记录;已有资源申请使用指定的 Database,
检查资源未绑定且可用,不增加反向授权名单。动态创建重试遇到同名记录时需核对身份与目标。
2. 使用 resourceVersion 并发控制,先在 Database 记录 Tenant namespace/name/UID。
已绑定其他 Tenant 时报告 Conflict,不抢占;版本冲突后重新读取、重新判断。
3. 在 Tenant status 记录 Database name/UID。若上一步成功、本步失败,后续 reconcile
核对身份后补齐,不回滚已经成功的资源侧绑定。
4. 双向记录一致才允许动态供应或凭据交付;实际数据库与凭据验证通过后才可 Ready。
此顺序借鉴 PV/PVC 的资源侧先写模式,行为依据见
[系统规格](specification.md#5-动态供应与排他绑定)。Retain 释放时保留旧绑定身份并进入
Released,不自动清空后重新分配。API 记录部分写入由 reconcile 重试;外部创建结果
无法确认时仍报告 Conflict,交给人工,不新增事务队列或 registry。
## 绑定协议剩余评审要求
实现前必须明确:
1. 引用字段的最终格式,以及落实管理员资源管理、controller 协调与普通申请权限的 RBAC 规则。
2. 绑定记录的最终字段及校验规则,落实上述写入顺序与恢复行为。
3. Tenant UID 变化、对象删除、Released 旧引用与管理员重新授权的判断。
4. 同一物理数据库重复登记的冲突处理;列表查询不是原子认领。
5. 已有凭据关联字段、旧访问处置与投射清理顺序;动态凭据路径已确定按 Database UID 定位。
6. Tenant/Database finalizer 配合;回收策略默认 Retain,删除流程前可改,进入后固定。
资源管理者设置 Delete 即表示删除授权,不增加额外审批字段。导入显式关联已有凭据;
Released 不自动改密,管理员处理旧访问后才重新开放。Tenant 不得自选任意 OpenBao 路径。
该协议使用 Kubernetes API 持久化,不为它新增 PostgreSQL registry。
未完成一致绑定不得供应或交付;外部创建结果不确定按 Conflict 人工处理。
## Conditions
每类资源至少提供唯一的 Ready;同类型 Condition 不重复,维护 lastTransitionTime 与
observedGeneration。最低安全错误分类见 [系统规格](specification.md#10-conditions-与可观测性)。
Conflict 必须说明目标、步骤、已确认与不确定部分及人工核实建议;不能建议清空 status、
伪造 Ready 或改密码绕过。可恢复依赖故障退避重试,冲突不忙循环。
具体 Reason 在 API 切片固定,Released 不得误报为可立即交付。
## 实现验收
CRD defaulting、CEL、status subresource、resourceVersion、并发绑定、重启与依赖 watch
使用真实 API server 验证;ownerReference/namespace 删除的实际 GC 使用测试集群。
类型、CRD、sample 和 contract tests 必须一起对齐,不以 fake client 替代 API 语义。
+58
View File
@@ -0,0 +1,58 @@
# Database 系统架构
本页解释 [系统规格](specification.md) 的组件边界;资源与申请分离的依据见
[ADR-0009](../decisions/0009-database-resource-and-claim.md)。设计已确认,运行链路尚未完成。
## 资源与后端
```text
Kubernetes API
Instance ──引用── Database ──排他绑定── Tenant
|
Ayatori Database controllers
| | |
PostgreSQL OpenBao ExternalSecret
catalog |
ESO → Secret
```
Kubernetes 保存声明、绑定与操作进度;PostgreSQL 保存实际数据库状态;OpenBao 保存应用凭据。
不在 PostgreSQL 中再建立管理 registry。Instance 是管理入口而非 CSI 协议实现,adapter 是薄访问层。
Instance 验证当前目标的管理能力,不供应 Tenant 数据库,不因 registry 缺失初始化任何 schema。
Database 用例负责独立资源的供应、显式导入、保留与回收;Tenant 用例负责申请、绑定与凭据交付。
这是用例职责,不要求为每一步新增一个 controller 或通用控制循环。
## 协调与恢复
通过 Kubernetes API 的 resourceVersion 保护并发更新,watch 推动依赖恢复;外部操作前保存
资源与意图,执行后观察并记录结果。数据库实际操作仍须处理后端竞态,列表检查不是唯一约束。
可靠确认的步骤允许幂等继续;外部创建与进度写入之间的失败若导致归属不确定,则停止写入并
报告 Conflict。不创建第二套所有权存储,不承诺跨系统事务或任意 status 丢失自动认领。
错误必须提供人工可用的步骤、资源与结果确定性信息,但不泄漏秘密。
## 绑定、导入与回收
Database 独立于 Tenant 存在,不能以会导致级联删除的 ownerReference 连接两者。
Instance 删除检查 Database 引用,包括 Released 资源,不直接清理数据库。
动态供应和管理员导入使用同一种资源记录。导入验证初始只读;未知同名数据库仍为冲突。
Retain 保留资源及旧绑定身份;重新绑定必须经过人工数据和访问权限处置,不自动分配。
Delete 由资源侧明确授权,在 finalizer 保护下按管理范围清理并逐步回读。
角色、凭据与投射的具体管理字段和清理顺序仍需 API 评审,不能用 PV 类比代替数据库权限设计。
## 保持的访问边界
管理 Secret 固定在 controller namespace,管理员维护其 ExternalSecret;controller 只读。
有效值变化刷新管理连接,不直接访问 Bao 获取管理凭据,不自行实现连接池。
应用凭据写 OpenBao,由 ESO 投射;controller 不直接写明文 Secret。
TLS、DNS/IP SAN、七键凭据输出和最小权限合同继续适用。
## 导航
- [API 合同与待细化字段](api-reference.md)
- [领域模型](domain-model.md)与[Instance 规格](domain-instance.md)
- [部署](deployment.md)、[安全](security.md)、[开发测试](development.md)
- [导入与迁移](migration.md)、[运维](operations.md)
+183
View File
@@ -0,0 +1,183 @@
# 部署与配置
> 本页区分已实现的 Instance 观测、认证和凭据准备配置,与尚未接入的 PostgreSQL 供应/交付合同。
> 完整 Database 服务仍不可部署使用;当前可执行入口见 [模块说明](README.md)。
| 项目 | 内容 |
| --- | --- |
| 状态 | Review |
| 环境 | homelab Kubernetes + 外部 PostgreSQL/OpenBao |
| 最后更新 | 2026-09-25 |
本文定义 v1alpha1 的运行依赖、启动顺序和部署级配置。Instance 观测已接入 manager;
OpenBao 认证可显式启用;ESO 与完整供应装配仍是后续实现合同。
## 依赖与顺序
1. 准备 PostgreSQL VM、持久盘、备份和网络入口。
2. 用 OpenBao PKI 签发 PostgreSQL server 证书,包含 Instance `host` 的 DNS SAN 与
`hostaddr` 的 IP SAN;配置 PostgreSQL 强制 TLS。
3. 创建 PostgreSQL controller 管理 role 和管理 database 连接权限。
4. 在 OpenBao KV v2 写入管理 role 凭据。
5. 配置 OpenBao Kubernetes auth、controller policy 和面向 ESO 的读取 policy。
6. 安装 ESO,配置独立的管理凭据同步身份和租户凭据读取身份。管理员在 controller
namespace 创建管理 ExternalSecret,确认管理 Secret 已同步;另创建供租户使用的
`ClusterSecretStore`。
7. 创建公开 CA bundle ConfigMap,并挂载到 controller 和需要直接验证数据库的应用。
8. 部署 controller,再创建 Instance;等待 Ready 后才创建 Tenant。
任何一步都不得把真实密码、Token、kubeconfig 或 CA 私钥提交进 Git。
## Controller 配置合同
当前 manager 支持 `--database-secret-namespace`(默认 `POD_NAMESPACE`,为空则停用
Instance 观测)与 `--database-root-cert`(公开 PostgreSQL CA PEM 路径)。Deployment
通过 downward API 获取 namespace,Secret 权限由该 namespace 的 Role 授予。
以下表格区分已实现的认证参数与尚待实现的供应/交付参数。
必填项缺失、路径无效或 duration 不为正数时,进程必须在启动 manager 前失败;
不得等到 reconcile 时才逐个资源报告配置错误。
| CLI flag | 必填/默认 | 说明 |
| --- | --- | --- |
| `--openbao-address` | 已实现,默认空 | HTTPS API 地址;为空时关闭认证会话 |
| `--openbao-consumer-address` | 默认同 `--openbao-address` | 写入 Tenant status,必须能被预期外部消费者解析 |
| `--openbao-auth-mount` | 已实现,`kubernetes` | Kubernetes auth mount 名称 |
| `--openbao-auth-role` | 已实现,启用时必填 | OpenBao 登录 role |
| `--openbao-ca-cert` | 已实现,默认系统信任根 | OpenBao 公开 CA PEM 路径 |
| `--database-credential-mount` | 已实现,默认空 | 显式设置后启用应用凭据准备,要求已配置 OpenBao 认证 |
| `--openbao-service-account-namespace` | 已实现,启用时必填 | TokenRequest 目标 SA 的固定 namespace |
| `--openbao-service-account-name` | 已实现,启用时必填 | TokenRequest 目标 SA 名称 |
| `--openbao-token-audience` | 已实现,`openbao` | SA JWT audience,必须匹配 OpenBao role |
| `--database-credential-prefix` | 已实现,默认 `applications` | Database UID 路径的 mount-relative 前缀;不迁移已有位置 |
| `--external-secret-store-name` | 必填 | controller 创建的 ExternalSecret 固定引用 |
| `--database-root-cert` | 已实现 | 只读 PEM trust bundle,不含私钥;沿用 Instance 连接配置 |
| `--reconcile-timeout` | `30s` | 单轮 reconcile 中外部操作的总期限,必须大于零 |
address 必须是绝对 `http` 或 `https` URL,不允许 userinfo、query 或 fragment,末尾 `/`
在规范化后移除。mount、auth mount 和 base path 都使用 mount-relative path 语义,不以
`/` 开头,不含空段、`.` 或 `..`;base path 还不得编码 KV v2 的 `data`/`metadata`
API 层。生产环境的 `--openbao-address` 必须使用 HTTPS;HTTP 只用于明确的开发 fixture。
### 集群内与 systemd 共用 Kubernetes 认证
controller 是 API 客户端,不要求部署为 Pod。OpenBao 认证直接复用 manager 已加载的
Kubernetes 配置:集群外可用标准 `--kubeconfig`(或 `KUBECONFIG`),集群内可使用
in-cluster 配置。禁止另要求 `/var/run/secrets/.../token` 文件或解析 kubeconfig 中的 bearer token。
每次 Bao 登录前通过 TokenRequest 申请新的短期 SA JWT,Kubernetes JWT 与 Bao token 的
生命周期分别由 API 签发和官方 SDK 续期管理;不把 kubeconfig 本身当成永久有效凭据。
管理员为 controller 的实际 Kubernetes 身份授予目标 namespace 内
`create serviceaccounts/token`,用 `resourceNames` 限定目标 SA;示例见
[最小 RBAC](../../config/samples/database_openbao_auth_rbac.yaml)。集群外 RoleBinding subject
对应 kubeconfig 的用户/组,集群内可绑定 manager SA;登录目标 SA 可以独立于调用者身份。
controller 不创建 SA、Role/RoleBinding,也不向自己授予权限。不要求 `get secrets` 来获取 JWT。
OpenBao role 还需限制 SA 名称、namespace 与 audience,TokenReview reviewer 身份由管理员配置。
示例启动参数(仅示意,不包含真实 kubeconfig 或凭据):
```sh
manager --kubeconfig=/etc/ayatori/controller.kubeconfig \
--database-secret-namespace=ayatori-system \
--openbao-address=https://bao.example:8200 \
--openbao-auth-role=ayatori-database \
--openbao-service-account-namespace=ayatori-system \
--openbao-service-account-name=database-openbao-login
```
这里的认证成功只开放 manager readiness,不代表 Database 已具备供应或交付能力。
实例管理 Secret 的 namespace 同样由参数指定,systemd 模式不依赖 `POD_NAMESPACE` 环境变量。
Tenant 不能选择任意凭据路径。凭据必须能随 Database 保留并安全交付给被授权的新 Tenant;
原 `<base-path>/<namespace>/<metadata.name>` 定位规则不再直接作为新 API 合同。
动态供应位置使用 `<base-path>/<Database UID>`;导入使用 Database 的显式 credentialRef,
不要求搬迁已有凭据。供应流程须先记录原 mount/path,不能在配置变化后重新推导位置。
consumer URL 仍使用无认证信息的 KV v2 API URL。
base path 必须是合法 mount-relative path,不以 `/` 开头且不包含空段、`.`、`..`、
`data`/`metadata` API 层。ExternalSecret 固定命名为
`<instanceRef>-<metadata.name>-postgresql`;目标 Secret 可由 Tenant 指定,但名称必须
满足 Kubernetes Secret 名称校验,不限制命名内容,默认与 ExternalSecret 同名。
配置变化不得隐式迁移既有凭据。修改 KV mount/base path 或 consumer address 前必须
停止 controller、评估现有 Database 与绑定,并走明确迁移。资源记录应能定位原凭据,
不能根据新部署参数静默切换;不再使用 registry 保存安装身份。
## PostgreSQL 管理 role
生产部署禁止使用 superuser。管理 role 至少需要:
- 连接管理 database、读取必要 catalog;
- 创建/修改受管 login role;
- 创建 database 并指定 owner;
- 撤销 `PUBLIC` CONNECT、授予租户 role CONNECT;
- 连接租户 database 并创建实例实际支持、租户申请的 extension;
- `Delete` 时禁止连接、终止目标 database session、删除已验证归属的 database/role。
部分 PostgreSQL 操作天然要求较高权限,尤其终止其他 session 和安装某些 extension。
第一版使用原生非 superuser 的 CREATEDB/CREATEROLE 方案,不引入 SECURITY DEFINER
管理接口。对自行创建的 owner 显式建立 SET membership,再以 owner 管理 ACL 与扩展;
已有对象仍须逐资源核实授权,不能凭基础属性接管。需要 superuser 的扩展不能扩大 controller
权限。真实权限矩阵见 [Instance 原生管理观测](README.md#instance-原生管理观测)。
## OpenBao 与 ESO
当前凭据准备只需固定前缀下 KV v2 data 的 create/read/update 权限,不需要 metadata 或
delete 权限,也不读取管理凭据路径。示例(`secret` 为测试 mount,需替换为实际配置):
```hcl
path "secret/data/applications/*" {
capabilities = ["create", "read", "update"]
}
```
在上面的认证启动参数基础上增加 `--database-credential-mount=secret` 即启用凭据准备;
需要不同前缀时同时配置 `--database-credential-prefix` 和对应 policy。仅设置
`--openbao-address` 不会启用凭据创建。缺少认证配置或非法 mount/prefix 在启动时失败。
新字段 CRD 必须先升级再启用 controller,否则 API pruning 会使确认记录无法保存。
未来 Delete 清理才需要永久删除全部版本及 metadata 的权限,本切片不提前授予。
管理凭据由管理员维护的 ExternalSecret 同步到 controller namespace;其 ESO 身份
只读对应管理路径,不能供 Tenant 使用。租户 ESO 身份只读 tenant base path,不得
读取 PostgreSQL 管理凭据。controller 不创建或修改管理 ExternalSecret/Secret。
`ClusterSecretStore` 由平台管理员创建,controller 只引用,不创建或修改 Store。
controller 创建的 ExternalSecret 与 Tenant 同 namespace;其 ownerReference 和 Retain 时的
保留/清理须与凭据交付协议一起确定,不能把投射关系等同于 Database 的 GC 关系。目标
Secret 包含固定七键:`username`、`password`、`database`、`host`、`hostaddr`、`port`、
`sslmode`。
## Kubernetes RBAC
- controller 按用例读/写 Instance、Database、Tenant 及其 status/finalizer 和 Event。
- Database 不设置随 Tenant 级联删除的 ownerReference;导入、预留、回收和重新绑定授权限管理员。
- controller 可在 Tenant namespace 创建、读取、更新、删除 ExternalSecret,并只读检查
对应 Secret 是否完成投射。
- namespace 用户可以管理本 namespace Tenant,但不能管理 Instance、Store、controller
配置或其他 namespace 的 ExternalSecret。
- controller 只在自身 namespace 读取所引用管理 Secret 的 data,不获得跨 namespace
的管理 Secret 读取权限。Instance 不允许自选 Secret namespace。
- 对应用目标 Secret,controller 无需读取 data;验证登录使用从 OpenBao 读取的应用
凭据,只检查 Secret 存在性和 ESO 状态。
## 升级与回滚
v1alpha1 尚不承诺跨版本转换。升级前备份 CR/绑定、PostgreSQL 数据与 OpenBao,
先在隔离 Kind 环境运行 E2E。禁止在同一组 CR 上同时运行两个 controller 版本。若新版本
在执行任何破坏性迁移前失败,可回滚镜像;涉及 API/storage 或凭据定位迁移时,
必须先写独立升级规格和回滚步骤。
## 上线验证
```text
PostgreSQL TLS 与备份验证
-> OpenBao auth/policy 验证
-> ClusterSecretStore Ready
-> controller Ready/leader elected
-> Instance Ready
-> 测试 Tenant Ready
-> DNS host 与 IP hostaddr 分别登录
-> 删除测试 Tenant 并验证所选策略
```
生产 homelab 上线前还必须完成 [`security.md`](security.md) 的权限检查和
[`operations.md`](operations.md) 的备份/逃生检查。
+269
View File
@@ -0,0 +1,269 @@
# 开发与测试环境
> 本页迁入作为 Database 模块的测试分层与 fixture 合同。旧项目的 Make target、devcontainer
> 和脚手架版本尚未适配 Ayatori;实现时应复用 Ayatori 现有工具链,并保持这里定义的测试边界。
## 当前设计验收(2026-09-24)
[ADR-0009](../decisions/0009-database-resource-and-claim.md) 将资源生命周期从 Tenant 中分离。
新增验收矩阵见 [系统规格](specification.md#11-验收)。registry 实现、专属测试与迁移依赖已撤除,不继续 schema 审计或自动所有权恢复切片。
| 层次 | 本次设计要求 |
| --- | --- |
| 纯领域 | Instance 无 registry 就绪判定、排他绑定、Released 不自动复用、管理范围与冲突规则 |
| envtest | 三资源 schema/status、RBAC、resourceVersion 并发、绑定单边更新/重启、依赖 watch、finalizer |
| 真实 PostgreSQL/OpenBao | 同名不修改、显式导入只读验证、创建不确定报冲突、可靠步骤幂等、Delete 故障重试 |
| 测试集群 | Tenant/namespace 删除不 GC Database、ESO 交付/释放、人工重新绑定前的旧访问处置 |
需故障注入外部成功而 API 写入失败、后端响应丢失、双 Tenant 竞争、同名新 UID、依赖稍后出现、
Instance 删除与 Released 引用。冲突必须给出可操作而不泄密的诊断;不要求自动认领不确定结果。
envtest 不运行 GC 或 ESO;这些行为必须由测试集群验证。
三资源 API 类型与 CRD 已实现,`make test` 包含真实 API server 验证:作用域、
静态默认值、非法声明、status 写入隔离、Condition 唯一性、resourceVersion 冲突与绑定记录回读。
另有绑定 controller 的真实 API server 测试:动态记录幂等、资源侧写入后故障注入、
新 reconciler 回读补齐、双 Tenant 竞争、Released/旧 UID 拒绝、目标固定、陈旧观察、
删除期间 finalizer 保留。实际 manager 在生成的 RBAC 角色下通过 watch/cache 处理依赖
稍后出现,绑定角色不具备 Secret 读取权限。没有 PostgreSQL/OpenBao 写入;后端 Ready
在测试中由 fixture 提供,不能把它当成完整 Instance/Database 观察验证。
Retain 释放和 Delete 清理仍未实现,当前删除会保持 DeletionPending 与 finalizer。
真实后端供应、凭据交付与上述完整删除矩阵仍待后续切片验收。
绑定规则另有不依赖 Kubernetes 的单元测试;service 测试只验证操作顺序、失败停止与最终
身份回读,不模拟 API server。原真实 API controller 测试覆盖完整分层调用,另验证 service
返回后发生并发修改时,资源呈现拒绝过期结果,重试完成绑定且保留其他字段。
## Ayatori 已接入的凭据与 metadata 切片测试
本节命令已在 Ayatori 接入;以下历史 Compose/Kind 操作仍属于迁入的目标合同。
```sh
make test
make lint
make lint-database-integration
make test-database-integration
```
最后一项要求本机 Docker 可用。它启动 envtest 的真实 API server/etcd 和固定镜像摘要的临时
PostgreSQL 容器,随机绑定回环端口,不读取 kubeconfig,也不接受指向现有数据库的 DSN。
每个凭据场景使用独立环境;测试清理仅关闭自己的进程和按确切 ID 删除自己的容器。
TLS 测试在临时目录生成一次性证书与私钥,不使用生产 CA。
当前覆盖固定 namespace 的 Secret 读取与 RBAC、缺失/无效凭据恢复、有效凭据变化后的重连、
无关字段更新不重连、中途轮换时丢弃观察、会话重建、并发读取、本地连接释放和 TLS 验证。
快速测试、lint 和 Database 集成测试均使用 Pod runner。按维护者于 2026-09-21 更新的接口
约定,runner 提供默认可用的 Docker;workflow 通过 `docker version` 和 `docker info` 预检,
不自行启动 daemon、不强制 storage driver 或覆盖 Docker endpoint。该约定的 CI 验收依赖
runner 后端修复上线,不能从本地测试通过推断远端已经可用。
fixture 启动失败会保留退出错误与 stderr,并遮蔽测试密码,
以区分缺少命令、daemon 不可达、权限和镜像拉取失败。
metadata 测试验证版本与可用扩展的只读查询,包括未安装扩展、大小写保持、search_path 遮蔽、
低权限账号读取、catalog 访问被撤回后的失败与恢复,以及凭据中途变化时同时丢弃版本和扩展。
权限撤回只修改每个场景自建 PostgreSQL 容器的 ACL;不连接现有服务。
可用列表不等于安装权限,这些检查不替代后续的完整管理权限矩阵或 Instance Ready 验收。
Instance 领域测试不再提供 registry 状态,初次验证与 Ready 重验分别覆盖所有管理检查项的
未观察、不可用、认证失败、权限不足及未知值,并验证依赖恢复;完整管理观察可直接 Ready。
PostgreSQL adapter 测试不包含 controller、Secret watch、status/finalizer 事件链、权限探测矩阵、ESO 或 Tenant 供应。
CRD 基础语义由前述 API 测试覆盖;版本查询成功不意味着 Instance Ready。
本项目同时依赖 Kubernetes API、PostgreSQL、OpenBao 和 ESO。日常开发不连接 homelab
中的真实服务:Kubernetes 使用 envtest 或一次性 Kind,另外两个依赖使用一次性
容器。这样既避免污染真实数据,也能把启动顺序固化为命令。
## 是否需要开发 VM
默认不需要。仓库的 devcontainer 使用独立 Docker-in-Docker daemon,Go 工具链、
Kind 节点和依赖容器都与宿主机环境隔离。宿主机只需要能够运行支持 privileged
container 的 Docker/Dev Container 环境。
只有以下情况才建议增加一台可随时重建的开发 VM:
- 宿主机不允许 privileged devcontainer;
- 无法安全使用 Docker socket 或 Docker-in-Docker;
- 本机地址段与 Kind/Docker 网络持续冲突;
- 需要长期运行、接近 homelab 网络和 TLS 配置的验收环境。
即使使用 VM,也应在 VM 内继续执行本文相同的容器化流程;不要把 VM 配置成第二套
手工维护的开发环境。
## 环境分层
| 层次 | Kubernetes | PostgreSQL / OpenBao | 用途 |
| --- | --- | --- | --- |
| 单元测试 | fake client | fake client | SQL 计划、状态转换和错误分类 |
| controller 集成测试 | envtest | fake adapter | CRD、watch、status、finalizer、ExternalSecret 对象 |
| adapter 集成测试 | 不需要 | Docker Compose | 真实协议、权限和幂等行为 |
| E2E | 一次性 Kind + ESO | Kind 内测试实例 | 凭据投射、TLS、完整网络和删除路径 |
envtest 只启动 API server 和 etcd,没有 kubelet、scheduler、ESO 或 controller-manager,
因此不能用它验证 Deployment、Pod 调度或 Service 网络。此类行为必须留给 Kind
E2E。
## 首次准备
推荐用支持 Dev Containers 的编辑器打开仓库。devcontainer 会提供 Go、Docker、
Kubebuilder、Kind 和 kubectl。脚本固定 Kubebuilder 4.15.0、Kind 0.33.0 和
kubectl 1.36.0,与当前脚手架和 Kubernetes Go module 对齐。容器启动后先确认:
```sh
go version
docker info
kubebuilder version
kind version
kubectl version --client
```
不要在仓库中保存真实 OpenBao Token、数据库密码或 kubeconfig。Compose 中的
`postgres-dev-only` 和 `dev-only-root-token` 是仅绑定回环地址、随容器销毁的公开
测试值,不得复制到其他环境。
## 日常开发的正确顺序
### 1. 生成并验证纯 Go/Kubernetes 部分
```sh
make manifests generate
make test
make lint
```
`make test` 会下载与 `go.mod` 中 Kubernetes minor 版本匹配的 envtest 二进制,
启动临时 API server/etcd,测试结束后自动关闭。
规格实现后,快速测试必须覆盖默认值/校验、Condition `observedGeneration`、三资源的
状态与绑定、不可变字段、extension 只追加、冲突和外部错误分类。envtest 只断言 controller 创建了正确
的 ExternalSecret;它不能证明 ESO 已生成 Secret。
### 2. 启动 PostgreSQL/OpenBao adapter 依赖
只有开发 PostgreSQL/OpenBao adapter 或完整 reconcile 时才需要:
```sh
make dev-up
make dev-smoke
```
启动顺序由 Compose healthcheck 保证:
1. 创建独立 Compose 网络;
2. 启动 PostgreSQL 和 OpenBao;
3. 等待 PostgreSQL `pg_isready` 成功;
4. 等待 OpenBao `bao status` 成功;
5. smoke test 执行 `SELECT 1`;
6. smoke test 在 OpenBao dev server 默认的 `secret/` KV v2 mount 写入并读回测试管理
凭据。
本机进程使用以下端点:
```text
PostgreSQL: postgresql://postgres:[email protected]:15432/postgres
OpenBao: http://127.0.0.1:18200
Token: dev-only-root-token
```
若端口冲突,可以只对当前命令覆盖:
```sh
POSTGRES_DEV_PORT=25432 OPENBAO_DEV_PORT=28200 make dev-up
```
后续执行 `dev-smoke` 和 controller 时必须使用相同端口变量。
Compose 使用明文 PostgreSQL/OpenBao dev 模式,不覆盖生产 TLS 合同。DNS SAN、IP SAN、
Kubernetes auth、最小 policy 和 ESO 必须在 Kind E2E fixture 中验证。
### 3. 运行针对临时依赖的测试或 controller
adapter 集成测试通过独立 Make target 执行,不默认塞进快速单元测试:
```sh
make test-integration
```
该 target 会启动一次性 Compose 依赖,并通过 `POSTGRES_TEST_DSN` 把测试指向开发
PostgreSQL。这是旧环境设计,不是当前 Ayatori 入口;当前 fixture 不接受外部 DSN,
使用本页前部的 `make test-database-integration`,禁止把测试指向真实 homelab database。
Gitea Actions 的 job 本身运行在 Docker container 中,不能通过 `127.0.0.1` 访问
Docker host 上发布的 Compose 端口。CI 会暂时将 job container 加入 Compose 网络,
并通过 `postgres:5432` 运行集成测试;清理前先断开该网络,才能删除 Compose
network。本机执行仍使用默认的 `127.0.0.1:15432`。
本机运行 controller 时,先确认当前 kubeconfig 指向专用 Kind,而不是真实 homelab:
```sh
kubectl config current-context
make setup-test-e2e
kubectl config current-context
make install
make run
```
此时 controller 运行在开发容器内,可以直接访问上面的回环端口。若要验证 Tenant
Ready,专用 Kind 还必须安装 ESO、创建测试 ClusterSecretStore,并让 Kind workload
能够访问测试 OpenBao。不要把包含
`127.0.0.1` 端点的样例部署到 Kind 内;Pod 中的回环地址只指向 Pod 自身。
### 4. 清理
```sh
make dev-down
make cleanup-test-e2e
```
`dev-down` 会删除 Compose volume;所有数据库和 OpenBao dev 数据都应视为一次性。
## E2E 顺序
CI 的 E2E 与本机 `make run` 不同:controller 会作为 Pod 运行在 Kind 中。因此完整
E2E fixture 必须把测试 PostgreSQL、OpenBao 和 ESO 部署进 Kind,并等待依赖 Ready 后
再创建 `PostgreSQLInstance` 和 `PostgreSQLTenant`:
```text
创建 Kind
-> 安装 CRD
-> 部署 PostgreSQL/OpenBao fixture,签发含 DNS/IP SAN 的测试证书
-> 安装 ESO,配置 OpenBao auth/policy 和 ClusterSecretStore
-> 等待依赖 Ready 并写入测试管理凭据
-> 构建并加载 controller image
-> 部署 controller
-> 创建 Instance
-> 等待 Instance Ready
-> 创建 Tenant
-> 等待 Tenant Ready
-> 验证 Database 绑定、PostgreSQL catalog、OpenBao KV、ExternalSecret 和 Secret
-> 分别使用 DNS host 与 IP hostaddr 登录
-> 删除 Tenant 并分别验证 Retain 与 Delete(含故障点重试)
-> 删除 Kind
```
以上 Compose/Kind 流程来自旧项目的环境设计,不是 Ayatori 已实现的运行状态。
Ayatori 尚未完成 Instance/Database/Tenant controller 链路;当前可执行的切片命令以本页
前部为准,不能从旧脚手架或 adapter 测试推断完整生命周期已经通过。
## 测试数据与泄漏检查
- 只使用显眼的固定 canary 测试密码,测试后扫描日志、Event、Condition、metrics 和
CR dump,出现 canary 即失败。
- 每个写入阶段注入中断:已确认步骤继续且密码不变;结果不确定则停止并明确报告 Conflict。
- 缺少绑定/进度记录时不得凭同名外部对象恢复所有权;过期 phase 不得绕过实际观察。
- 为未知同名 database、role、Bao record 和伪造 COMMENT 分别构造 Conflict。
- Delete 在每个外部删除步骤失败后重试,确认未误删非当前 UID 资源。
- 迁移测试按 [`migration.md`](migration.md) 完整执行,不以单纯 `pg_restore` 成功代替
应用读写和回滚验证。
## 故障排查
查看依赖状态与日志:
```sh
docker compose -f hack/dev/compose.yaml ps
docker compose -f hack/dev/compose.yaml logs postgres openbao
```
如果 envtest 报端口监听失败,通常是当前执行环境禁止监听回环端口,而非 controller
失败;在 devcontainer 或允许本机监听的 runner 中执行。若 Kind 无法创建,先运行
`docker info`,确认当前用户可以访问 devcontainer 内的 Docker daemon。
+90
View File
@@ -0,0 +1,90 @@
# Instance 领域对象规格
日期:2026-09-25。资源模型修订依据
[ADR-0009](../decisions/0009-database-resource-and-claim.md),行为以
[系统规格](specification.md) 为准。本页替代原 registry 准备与恢复合同;领域依赖已撤除,Instance 应用/controller 观测链路已接入。
## 职责
Instance 是登记的 PostgreSQL 管理入口,只接收观察、判断规则,不直接或通过回调执行 IO。
应用层读 Secret、调用 adapter、关联目标与观察,再将结果交给领域判定。
领域不持有客户端、连接池、context 或完整 Database/Tenant 集合。
每轮由 CR 重建;连接可由应用层复用,但旧连接、旧 Ready 不是本轮能力证据。
身份与 endpoint 以管理员声明为准,变更使观察失效,不验证物理服务器连续性,
不迁移旧数据,不自动授权旧 UID 资源的操作。
## 字段与观察
| 内容 | 合同 |
| --- | --- |
| identity | Instance UID/name;同名新 UID 是新对象 |
| revision | 当前 generation,不能与旧观察混用 |
| definition | endpoint 与管理 Secret 引用,不含明文 |
| checkpoint | Pending、Validating、Ready、Deleting |
| observedRevision/readiness/version | 映射 CR status,只表示进度或最近结果 |
| availableExtensions | 本轮实际可用集合;未观察与空集合不同 |
| evidence | 本轮目标、版本与管理能力检查结果,不持久化为永久授权 |
| deleting | 删除请求;禁止新的供应 |
CapabilityObservation 包含目标(UID、generation、endpoint、凭据引用)、server version、
连接/metadata/role/database/grant/extension 管理检查项。各项区分成功、失败和未观察,
不包含 registry 状态、凭据或驱动错误。缺项或目标不匹配不得 Ready。
实际可用扩展不等于安装权限,不做 allowlist,不改大小写;查询失败不当作不支持,
列表变化不自动卸载。安装必须由实际操作及回读验证。
resourceVersion 留在应用层处理 API 并发,不是领域版本或物理数据库身份。
## 行为
- Reconstitute 校验 definition,重建 checkpoint,丢弃旧 evidence。
- BeginValidation 清空能力证据并进入 Validating。
- AssessManagement/AssessReadiness 根据本轮完整观察判断 Ready 或安全失败;
方法的具体合并方式在实现重构时决定,不保留无意义的中间初始化阶段。
- CheckExtensions 判定请求集合,不能单独授权供应。
- RequireProvisioningReady 检查本轮能力与删除状态;不授予 Database 所有权。
- BeginDeletion 禁止新供应,不执行外部删除。
- Snapshot 返回值副本,不序列化 evidence 或秘密。
撤销 PlanRegistryPreparation、AssessRegistryResult、RegistryPreparationResult、
RegistryState 与 InitializingRegistry 的设计需求。不能用始终返回 Usable 的兼容层绕过旧逻辑。
## 应用与连接边界
管理凭据来自 controller namespace 的 Secret;管理员维护 ExternalSecret,ESO 同步。
Instance 不直接访问 Bao。有效用户名/密码变化时应用层释放旧连接、用新值装配并重验;
metadata/无关字段变化不重建。采集途中有效值变化必须丢弃观察,不因 generation 没变而复用。
controller 不修改 PostgreSQL 密码、管理 Secret 或 Bao 管理凭据。
InspectManagement 是观察能力,不包含 schema 初始化/迁移。只读 metadata 查询仍不足以
证明管理权限;真实权限矩阵由 adapter 定义和测试。连接复用由 pgxpool 提供,不自建池。
已可用管理凭据下,Bao/ESO 当前故障不单独撤销 Instance Ready。
## 状态与删除
```text
Pending → Validating → Ready
Ready → Validating(配置/凭据变更或能力失效)
任意阶段 → Deleting
```
应用层保存意图、获取观察、领域判定、按 resourceVersion 保存结果;
保存冲突重新装载,不能覆盖较新配置。Instance 观察本身不创建外部资源,
初始 status 缺失可重新探测,不由此推出 Database 所有权可自动重建。
受管前先保存 finalizer。删除期间查询 Database 引用(包括 Released/删除中)及未绑定
Tenant;存在引用或查询失败都等待,无引用才解除。不得级联删除数据库或凭据。
finalizer 不阻止并发申请 CR 创建;新请求见 Instance 删除中/不存在时不得供应。
不引入跨对象锁,不声称列表与删除之间存在原子事务。
## 验收与实现差距
领域单测覆盖缺项、旧配置、错误目标、扩展空集合与未观察、重验与删除禁用。
真实 API server 验证 Secret、resourceVersion、watch、finalizer;真实 PostgreSQL 验证
权限、TLS、凭据更新和查询失败,不以领域布尔值或 server_version 查询代替管理权限验收。
Instance 领域代码、adapter 与测试的 registry 依赖已撤除。AssessManagement 根据完整观察
直接完成验证;AssessReadiness 失败进入 Validating,依赖恢复后重新验证。领域测试覆盖各检查项
在这两个入口的失败与恢复。原生权限检查、Instance controller、metadata-only Secret watch
和引用删除保护已接入,验证矩阵见 [模块说明](README.md#instance-原生管理观测);
Database/Tenant 的供应、交付和回收仍未完成。
+94
View File
@@ -0,0 +1,94 @@
# Database 领域模型
状态:资源模型已确认,字段与绑定协议待细化。日期:2026-09-25。
行为以 [系统规格](specification.md) 为准;决策依据见
[ADR-0009](../decisions/0009-database-resource-and-claim.md)。
## 统一语言与关系
| 术语 | 含义 |
| --- | --- |
| Instance | 平台登记的 PostgreSQL 资源来源与管理入口 |
| Database | 独立存在的数据库资源,保存目标、管理范围、绑定与回收策略 |
| Tenant | 用户对数据库的申请与使用合同 |
| Binding | Database 与 Tenant 的排他关联,不是独立 Claim 或 registry |
| LoginRole | 当前单数据库场景中兼任 owner 的登录角色 |
| CredentialLocation | 与资源生命周期一致的凭据定位,不是密码 |
| CredentialProjection | 面向当前使用者的凭据投射要求与观察 |
Instance 一对多 Database;每个 Database 同时零或一个 Tenant,Tenant 最多一个 Database。
Instance 不持有全部资源的内存集合。三者以引用关联,操作一个资源无需加载整个实例集合。
Instance 与 Database 是集群级资源,Tenant 位于 namespace。Tenant 按名称引用 Database,
Database 记录所绑定 Tenant 的 namespace/name/UID。Database 不属于应用 namespace;
平台管理员管理资源及回收策略,普通申请者不能自行将 Released 资源重新开放。
Database 的 instanceRef 表达资源归属,手工登记时也必须提供,不从 Tenant 反推。
Tenant 动态申请才选择 Instance;引用已有 Database 时使用资源声明的 Instance。
释放使用绑定不改变 Database 的实例归属,修改引用不能实现外部数据库迁移。
Database 不再是 Tenant 内的无独立生命周期描述。原 OwnershipClaim 不再作为独立领域能力:
排他绑定是资源自身的不变量,Kubernetes 保存记录,不另建 PostgreSQL 所有权存储。
## 职责
Instance 只接收观察并判断当前连接、metadata、管理权限和扩展支持,不访问 IO、不初始化
registry。管理凭据来源和连接刷新由应用层协调,连接池由 pgxpool 实现。见
[Instance 规格](domain-instance.md)。
Database 保护目标和管理范围、排他绑定、导入验证与 Retain/Delete 规则。资源首次外部操作前
必须已有持久记录;完成记录与外部存在性分别检查。数据库名称不是归属证明。
Tenant 表达申请与交付要求;可显式申请未绑定且可用的资源,不增加反向授权名单。
绑定后检查数据库满足要求、
应用凭据可登录且 ESO 投射完成,才可 Ready。Tenant 删除意味着释放使用关系。
第一版 Database 的生命周期边界包含一个 database、一个兼任 owner 的 LoginRole 及其
应用凭据。LoginRole 与 CredentialLocation 随 Database 保留,不能因为 Tenant 消失就
失去定位或未经授权被删除;导入时的管理授权与具体字段仍需评审。不新增 Role、Credential
或 Claim CRD,也不预留多账号集合;若出现一库多账号的实际需求,再通过后续 API 版本演进。
## 生命周期与恢复
- 动态创建与显式导入最终形成同一种 Database 资源,但导入本身不允许改密、改 owner 或删除。
- Retain 后 Database 保持 Released 与旧绑定身份,人工确认数据、权限和凭据后才可重新绑定。
- 回收策略属于资源侧;Tenant 与 Database 不是可随申请级联 GC 的父子关系。
- 回收策略默认 Retain,进入删除流程前可由资源管理者修改,进入后固定;Delete 无额外审批。
- 动态凭据位置按 Database UID 确定,导入显式关联已有凭据;Released 不自动改密。
- 绑定 UID 防止同名新申请继承权限。双向记录的单边写入不代表绑定完成。
- 动态 Database 名称由 Tenant UID 确定;先持久化资源侧 Tenant 引用,再更新 Tenant status
的 Database 引用。后一写入失败由 reconcile 核对身份后补齐,不回滚资源侧记录;
其他 Tenant 已占用则报冲突。双向一致后才供应或交付,绑定不等于 Ready。
- 普通失败按 reconcile 重试;可靠确认的步骤幂等继续;不确定创建/未知同名对象报告 Conflict。
- Kubernetes status 是持久进度和观察,不是外部事实,也不是 controller 内存。
不引入“status 任意丢失后自动恢复所有权”的附加要求。
## 分层
| 层 | 责任 |
| --- | --- |
| 领域 | 值、身份、允许动作、不变量、完成与冲突判定;不做 IO |
| 应用 | 装载记录与事实、协调 API 更新和 adapter、回读、交回领域判定 |
| controller | watch/调度、调用用例、请求资源呈现与安排重试;不判断绑定资格 |
| adapter | Kubernetes 资源映射与呈现(含 conditions/status/finalizer)、后端访问与安全错误分类 |
| 装配 | 客户端与成熟连接池的生命周期,不是领域状态 |
不引入通用 Repository CRUD、跨系统 Unit of Work、事务队列或第二套 phase 存储。
resourceVersion 解决 API 对象并发更新,不宣称 PostgreSQL 与 Kubernetes 原子提交。
绑定实现中,`domain/binding` 承载请求默认值、资源身份匹配、实例就绪与排他绑定规则;
`application/BindingService` 协调固定申请、资源侧写入和回读确认,返回待呈现结果。
两层均不依赖 Kubernetes API 类型。`adapter/kubernetes/BindingResources` 将 CR 转换为事实
快照,并负责保留其他字段、检查快照版本、写入 finalizer 和呈现 Conditions/status。
controller 仅连接事件、service 与呈现层,不把资源写入细节和领域判断塞进 Reconcile。
这里的接口只列出绑定用例所需操作,不扩展成通用 CRUD、Repository 或事务框架。
## API 切片前需明确
- 引用与绑定字段的最终格式及校验、管理员与 controller 的权限落实。
- 资源侧回收策略与 Tenant/Database finalizer 配合。
- 导入时角色/凭据的管理范围及关联字段、旧使用者撤权及投射清理。
- 绑定/导入同一实际目标的重复声明如何拒绝,且不引入 registry。
- 管理员确认冲突、解除旧绑定的具体可审计操作入口。
这些细节不阻止已确认的三资源设计,但必须先于对应 API 与生命周期实现获得评审。
+147
View File
@@ -0,0 +1,147 @@
# 现有数据库导入与迁移 Runbook
| 项目 | 内容 |
| --- | --- |
| 状态 | Review;尚未在临时 PostgreSQL 演练 |
| 适用范围 | 管理员显式导入,或通过 dump/restore 迁移到新资源 |
| 最后更新 | 2026-09-24 |
当前设计支持管理员显式登记已有 Database;未知同名资源仍不得自动认领。
导入不要求移动数据,不隐含改密码、owner、授权或删除权限。API schema 与绑定协调已实现,
但导入观察和凭据交付尚未实现,以下导入步骤
是验收要求而非可直接执行的命令。
## 显式导入与保留资源复用
1. 核对 Instance、数据库、owner、角色权限、扩展、使用者与备份,确定允许管理的范围。
2. 由管理员声明 Database,指定已有目标,回收策略默认 Retain;导入验证初始只读。
3. 安全关联现有应用凭据;具体 API 待定,不把密码写入 CR,不因验证失败重置密码。
4. Tenant 显式引用未绑定且可用的 Database;controller 验证要求并建立排他绑定,无额外名单审批。
5. 验证实际登录与 ESO 交付;不满足时停止,不以修改原数据库作为默认修复。
Released 资源复用前另需核实旧使用者的访问权限、数据交接与投射处置。保留旧绑定身份直到
人工处理完成,不仅靠清空 claimRef 或修改 UID 授予新使用权。
导入失败时原数据库应保持不变;撤回登记不得触发 Delete。绑定后的回退按 Retain 释放,
检查新投射与访问授权的影响,不能承诺撤回 CR 自动恢复此前所有外部访问状态。
## 可选的 dump/restore 路径
不适合直接导入、需要改变 owner/权限模型或移动数据时,可使用下述逻辑迁移流程。
它不是纳管现有数据库的唯一路径;保留旧资源作为限时回滚点。
以下命令是顺序模板,不可原样复制到真实环境。先把尖括号变量解析成明确值,确认当前
连接目标,再逐条执行。dump 可能包含敏感业务数据,必须放在加密临时存储且不得提交 Git。
## 前置条件
- 已验证 PostgreSQL/OpenBao 备份和恢复;记录恢复点。
- Instance 已 Ready,目标 namespace 存在,ESO ClusterSecretStore Ready。
- 最终 database/login role 当前由旧应用占用,但改名后的保留名称、新推导的 Bao path
均不存在。
- 已记录旧 database owner、grants、extensions、locale/encoding、连接配置和验证清单。
- 已确认应用可停止写入,并确定回滚窗口和负责人。
- 已确认旧 login role 不被其他 database/应用共享,且角色改名不会破坏未纳入本次维护
的依赖。
## 迁移顺序
### 1. 盘点与预演
```sh
pg_dump --schema-only --no-owner --no-privileges \
--dbname='<old-admin-connection>' > schema-preview.sql
```
检查不受 v1alpha1 管理的对象:额外 roles、跨库依赖、FDW、large objects、订阅、显式
tablespace、owner/grant 和目标实例不支持的 extension。无法映射为单 database + 单 login
owner 的环境必须先人工简化,不能让 controller 猜测。
### 2. 创建一致性 dump
停止应用写入并确认活跃写事务结束,然后创建最终 custom-format dump:
```sh
pg_dump --format=custom --no-owner --no-privileges \
--file='<secure-temp>/tenant.dump' \
--dbname='<old-admin-connection>'
pg_restore --list '<secure-temp>/tenant.dump'
```
不要删除旧 database/role。记录停写时间、dump checksum 和 PostgreSQL 版本。
### 3. 释放最终名称
保持应用停写,终止旧 database 的应用连接。连接其他管理 database,以管理员身份把旧
database 和旧 login role 改为明确的保留名称:
```sql
ALTER DATABASE <old_database> RENAME TO <old_database>_retained_<timestamp>;
ALTER ROLE <old_login_role> RENAME TO <old_login_role>_retained_<timestamp>;
```
identifier 必须由管理员工具安全引用,不能把未经校验的值直接拼入 SQL。PostgreSQL 在
角色改名时会清除以旧角色名加盐的 MD5 密码;使用 MD5 的旧环境必须在维护前准备安全的
密码重设/回滚方法。SCRAM verifier 不受角色名改动影响,但仍须实际验证回滚登录。
### 4. 创建受管空目标
应用 `PostgreSQLTenant`,使用未被占用的 database/loginRole,等待 Ready。确认:
- Database/Instance 身份及 Tenant 排他绑定正确;
- OpenBao 凭据位置与 Database 管理范围及当前交付授权一致;
- ExternalSecret Ready 且目标 Secret 已投射;
- 新凭据可以通过 DNS host 和 IP hostaddr 分别登录空 database。
### 5. Restore
从 OpenBao 或目标 Secret 安全取得新应用凭据,不要把密码放进 shell history。以新 login
owner 连接目标 database:
```sh
pg_restore --exit-on-error --no-owner --no-privileges \
--dbname='<new-application-connection>' \
'<secure-temp>/tenant.dump'
```
extension 应由 Tenant spec 创建。若 dump 仍包含 extension 定义,预演必须确认 restore
行为幂等;目标实例不支持的 extension 必须在迁移前解决。
### 6. 验证并切换
- 对比关键 schema、表数、行数/校验和、sequence、function 和 migration version。
- 用新 login 验证读写、migration 和应用健康检查。
- 将应用配置切换到新 Secret 或 OpenBao URL,保持旧数据库只读/停写。
- 观察一个约定窗口,确认错误率、连接数和关键业务功能。
### 7. 收尾
回滚窗口结束后,按独立变更删除旧 database/role/旧凭据;它们不属于 controller,禁止
通过新 Database 的 `Delete` 清理。安全删除 dump 和临时凭据材料,并记录验证结果。
## 回滚
在新目标出现问题且旧资源仍保留时:
1. 立即停止新目标写入。
2. 评估切换后是否产生新数据;若有,先决定反向迁移或接受丢弃,不能盲目切回。
3. 将应用连接切回 retained database/role;若必须恢复原名称,先确保新受管目标已用
资源侧 `Delete` 完整清理或改用不同名称,再安全地反向执行 rename。
4. 恢复旧凭据(MD5 环境可能需要重设),验证旧服务。
5. 保留失败 Tenant 供排障;修改 Database 的 Retain/Delete 策略前明确其外部资源后果。
若已经删除旧资源,则只能使用已验证备份恢复,不再属于本 runbook 的快速回滚。
## 演练验收
发布首个可用版本前,必须在临时 PostgreSQL/OpenBao/Kind 环境执行本文并记录:
- 显式导入的管理权限、Released 重新开放前的旧访问处置和失败不修改原资源;
- 使用的 PostgreSQL major version 和命令版本;
- dump/restore 返回码和对象差异;
- DNS/IP TLS 登录结果;
- ESO 投射与应用启动结果;
- 回滚演练结果;
- 哪些命令或前置检查需要修订。
完成演练前,本文不得标记为 `Verified`。
+92
View File
@@ -0,0 +1,92 @@
# 运维与故障处理
状态:设计合同,操作入口待 API 实现与隔离环境演练。日期:2026-09-24。
依据 [系统规格](specification.md),不再查询或维护 PostgreSQL registry。
## 当前绑定切片的限制
源码已接入绑定、Instance 观测与可选的 Bao 凭据准备,未接入 PostgreSQL 供应、ESO 交付或删除清理。
Bound/BindingComplete 只表示 Kubernetes 双向记录一致,Ready 仍为 False。
Tenant 删除会保留 `database.ayatori.ddupan.top/tenant-protection` 并报告 DeletionPending;
Database 的 `database.ayatori.ddupan.top/database-protection` 也尚无清理后移除路径。
这是未完成能力的明确边界,不是已经实现的 Retain/Delete 恢复逻辑。不要将此切片部署为
业务 DBaaS,也不要为了消除等待状态直接移除 finalizer;后续必须补齐清理与验收。
凭据准备现在可单独启用,见[部署参数](deployment.md)。观察 Database 的
`CredentialsReady`、固定 `status.credentialRef` 和 `status.credentialVersion`,不要导出
凭据内容。Prepared 只代表 Bao 凭据可用,不代表已建库或 Tenant Ready。
`CreationStarted` 在创建结果确认前持久化;重启或失败留下该状态时报告 Conflict。
这包括“状态已写但请求还没发出”的保守停止;不要因为当前位置暂时为空就重试生成密码。
无确认版本的 Conflict 不会自行消失,后端恢复也不自动重入;应暂停该 controller、等待
在途请求结束后核实版本历史、残留与绑定,再决定清理或显式导入。不要清空整个 status
或更改固定引用来绕过保护。导入和完整人工恢复入口仍待对应切片实现。
## 日常检查
Instance 观察已实现:先确认 manager 配置了 `--database-secret-namespace` 或 `POD_NAMESPACE`,
再检查 Ready Reason、observedGeneration 与管理 Secret 名称/字段映射,切勿导出其 data。
`InsufficientPrivileges` 表示当前原生方案要求的非 superuser、CREATEDB/CREATEROLE 不满足;
`CredentialsChanged` 会丢弃中途轮换的结果并重验;`InstanceInUse` 消息定位阻塞删除的资源。
Secret 事件立即入队,30 秒重查覆盖 PostgreSQL 权限等没有 Kubernetes 事件的外部变化。
Instance 删除不要求 PostgreSQL 可达,但必须可读取所有 Database/Tenant 引用。
先看 Instance、Database、Tenant 的 Ready Condition、绑定 UID、阶段与 observedGeneration,
再核对 PostgreSQL catalog、OpenBao metadata、ExternalSecret 与 Secret 投射状态。
具体 kubectl 资源名、finalizer 名称与人工确认字段在 API 实现后补齐,不提供猜测的 patch 命令。
不得把 Secret data、密码或带 Token 的请求粘贴到 issue/日志。
## 故障分类
| Reason/状态 | 首要检查 |
| --- | --- |
| InvalidSpec / ImmutableField | 请求、名称、不可变目标与只追加扩展约束 |
| DependencyUnavailable | 网络、DNS、服务状态和超时;恢复后退避重试 |
| AuthenticationFailed | 管理 Secret、TLS、OpenBao auth |
| InsufficientPrivileges | PostgreSQL/OpenBao 权限与 Kubernetes RBAC |
| InstanceNotReady | 当前目标的管理能力,不检查 registry |
| Conflict | 绑定 UID、未知同名资源、失败步骤与外部结果确定性 |
| CredentialProjectionFailed | 授权的凭据位置、Store、ESO 与目标 Secret |
| Released | 资源已保留,不代表可直接交给另一个 Tenant |
## 创建不确定或同名冲突
1. 保留 CR、绑定与安全诊断,不清空 status、不反复删除重建申请。
2. 核对确切 Instance/database/role 和凭据位置;区分已确认完成与结果不确定的操作。
3. 使用只读检查确认资源内容、使用者和权限,不通过重设密码来“验证归属”。
4. 管理员决定清理确定的残留后重试,或显式导入保留资源;涉及删除需另有明确授权。
5. 记录处理依据,再按 API 的受控入口恢复协调。
普通依赖故障可以自动继续,未知归属不得因后端恢复就自动认领。
controller 重启保留 Kubernetes 中的记录,不需要恢复第二套 registry。
## Retain 与重新绑定
Tenant 删除后 Database 及实际资源保留,进入 Released,保存旧绑定身份。
不要删除 Database 对象来“释放名称”,也不要只修改 UID 或 Ready 强行交付。
管理员先确认数据是否允许交给新使用者、旧角色是否共享、旧账号访问如何撤销或保留、
新使用者如何获得凭据,以及原 ExternalSecret/Secret 的处置。删除 Secret 不会撤销已持有密码
的 PostgreSQL 访问。完成这些处置后,才通过显式授权重新绑定;不自动回到可分配状态。
具体凭据关联与解除绑定字段尚待 API 评审,当前不能宣称已有可执行恢复命令。
## Delete 卡住
核对 Database/Instance/绑定身份、资源侧 Delete 授权及实际管理范围,修复相关依赖,
让 controller 从已确认的步骤继续。不要删除共享角色或未纳管凭据,不使用扩大范围的 CASCADE。
依赖永久丢失时列出每个可能残留的数据库、角色、凭据与投射。只有管理员接受残留与后续处置
责任后才人工移除确切对象的 finalizer。该操作不会完成清理,也不会授权新申请使用残留资源。
## 备份与恢复
分别备份 PostgreSQL 数据、Kubernetes 资源与绑定记录、OpenBao 数据及必要配置。
不再要求备份专用 registry。只复制在线磁盘不等于有效数据库备份;秘密备份必须加密并限制访问。
灾难恢复先暂停 controller,核对三者恢复点、UID、外部目标与凭据的一致性,再恢复协调。
不一致时按 Conflict 人工处理,不承诺仅凭外部同名数据库重建丢失绑定。
在隔离环境演练登录、导入、Retain、重新绑定与 Delete 后才能标记验证通过。
## 紧急停止
疑似越权删除或秘密泄漏时暂停 controller,保留 CR 与脱敏证据,限制相关管理身份权限,
在隔离环境复现并确认修复后恢复。一般依赖失败不需要停机。
+81
View File
@@ -0,0 +1,81 @@
# 安全模型
| 项目 | 内容 |
| --- | --- |
| 状态 | Review |
| 最后更新 | 2026-09-25 |
## 保护目标
- 应用密码只存在于 OpenBao、ESO 投射的目标 Secret 和需要使用它的进程内存中。
- controller 只能在 Database 已确认的管理范围和当前绑定/操作授权内修改资源;未知同名对象报冲突。
- namespace 租户不能越权管理 Instance、Database 导入/回收、其他 namespace 或 controller 配置。
- PostgreSQL 和 OpenBao 的网络身份使用受信 CA 验证,不因 DNS 不可用而降级 TLS。
## 信任边界
Kubernetes 管理员、OpenBao 管理员和 PostgreSQL 管理员是平台信任主体。能读取 Tenant
目标 Secret 或对应 OpenBao path 的主体等同于持有数据库账号。database owner 可以
改变自己 database 内的对象,因此 COMMENT 不能作为 controller 所有权依据。
数据库备份包含业务数据;Kubernetes 保存资源与绑定记录;OpenBao 保存应用凭据。
完整灾难恢复必须分别保护三者并核对恢复点,不依靠数据库内 registry 重建绑定。
## 凭据处理
- controller 使用 Kubernetes auth 获取短期 OpenBao token,不配置长期静态 token。
- Kubernetes auth 不等于部署在 Kubernetes 内:复用 manager 的 kubeconfig/in-cluster 身份,
通过最小 RBAC 的指定 ServiceAccount TokenRequest 获取 JWT,不依赖 Pod 投射文件。
kubeconfig 的签发、更新与撤销由部署管理负责;申请失败不回退其他机器身份。
- 管理凭据只从 Instance 引用的 controller namespace Secret 读取,不复制到
CR/status/Event/metric/trace;管理员维护 ExternalSecret,由 ESO 同步该 Secret。
- 动态供应密码使用密码学安全随机源;已有可靠关联时复用 OpenBao 现值,结果不确定时停止并报冲突。
- controller 创建 ExternalSecret,不直接创建含 data/stringData 的 Secret。
- 日志字段允许 namespace/name、UID、generation、阶段和错误类别;禁止记录请求/响应体、
DSN、Authorization header、密码或完整 OpenBao URL path 作为 metric label。
- panic、错误包装和测试失败输出必须经过凭据泄漏测试。
## TLS
- homelab 默认 `verify-full`,`disable` 只允许显式开发配置。
- server 证书同时覆盖 DNS `host` 和 IP `hostaddr`;消费者自行选择连接目标。
- OpenBao PKI 保管 CA 私钥并负责签发/续期。controller Deployment 只挂载公开 CA
bundle,挂载只读且使用最小文件权限。
- 证书轮换必须先发布同时信任新旧 CA 的 bundle,再轮换服务端证书,最后移除旧 CA。
## 最小权限
OpenBao controller identity 只管理固定 tenant base path,不读取管理凭据。管理凭据
ESO 身份只读管理路径,租户 ESO 身份只读 tenant base path,二者隔离,Tenant 不得
使用管理凭据 Store。controller 对管理 Secret 的读取限于自身 namespace,Instance
不能指定其他 namespace;controller 不创建或修改管理 Secret/ExternalSecret。
2026-09-25 维护者确认第一版使用原生非 superuser 管理 role,具有 CREATEDB/CREATEROLE,
不引入 SECURITY DEFINER 接口。Instance 检查拒绝 superuser;具体已有资源的 owner 和
membership 仍需逐资源验证,不能把基础能力用于接管他人资源。扩展按实际权限安装,
不因可用列表包含某个扩展就默认能安装它。controller 不调用 shell 或 `psql` 拼接用户输入。
当前检查与真实权限矩阵见 [Instance 原生管理观测](README.md#instance-原生管理观测)。
Kubernetes RBAC 应把 Instance 管理、Database 导入、Released 重新开放和回收限制给平台管理员。
有权创建 Tenant 的申请者可显式申请未绑定且可用的 Database,不增加资源侧允许绑定名单
或逐 Tenant 审批。Released 必须先由管理员处理旧访问并重新开放。Tenant editor
不自动获得 Secret read;是否读取目标 Secret 由 namespace 内独立 RBAC 决定。
## 删除保护
资源侧 Delete 是明确的数据销毁授权,但仍必须在每一步校验 Instance/Database UID、
绑定、实际目标及角色/凭据管理范围;CR 中记录了意图不等于外部对象由本系统创建。
禁止对未知对象使用 `CASCADE`。删除 finalizer 卡住时只能按
[`operations.md`](operations.md) 核实外部状态后人工移除;该操作可能遗留资源。
## 发布前安全验收
- 使用错误 CA、错误 DNS 名和错误 IP 时连接失败;正确 DNS/IP SAN 均成功。
- namespace 用户不能修改 Instance 或跨 namespace Tenant/ExternalSecret。
- controller/ESO 的 OpenBao policy 互相隔离,越权请求被拒绝。
- 应用 login 不能创建 role/database,也不能连接其他租户 database。
- 日志、Event、Condition、metrics、CR 导出和测试 artifact 不含 canary password/token。
- 伪造 COMMENT、同名 database/role 或错误 UID metadata 均不能绕过 Conflict。
- Delete 只销毁 Database 已确认管理范围内且获得删除授权的资源。
- 导入检查不改密码/owner,Released 不自动授权新使用者;旧访问处理后才能重新交付。
- 导入默认 Retain;角色/凭据的管理与删除范围未明确时不得扩大操作范围。
+238
View File
@@ -0,0 +1,238 @@
# Ayatori Database 系统规格
| 项目 | 内容 |
| --- | --- |
| 状态 | 资源模型与生命周期已批准;字段协议待 API 评审 |
| 目标 API | `database.ayatori.ddupan.top/v1alpha1` |
| 最后更新 | 2026-09-25 |
| 决策 | [ADR-0009](../decisions/0009-database-resource-and-claim.md) |
本文是当前行为合同,替代旧的 Tenant 同时承担申请与资源生命周期、PostgreSQL registry
持久所有权、任意 status 丢失自动恢复的设计。批准设计不表示实现已完成。
未决字段不能由实现自行补成新产品约定。
## 1. 范围
在已存在的 PostgreSQL 实例上供应独立数据库、一个兼任 owner 的 login role、申请的扩展及
应用凭据;支持管理员显式导入已有数据库。Kubernetes API 管理声明与绑定,OpenBao 保存
应用凭据,ESO 向应用 namespace 投射 Secret。
不运行 PostgreSQL、VM、存储、备份或 OpenBao;不提供跨实例数据迁移、自动密码轮换、
多角色权限产品或跨系统事务。备份与数据恢复仍由管理员负责。
## 2. 资源与职责
```text
Instance
└─ Database × N 独立持久资源
└─ Tenant × 0..1 排他绑定的用户申请
```
| 资源 | 职责 | 不承担的职责 |
| --- | --- | --- |
| Instance | 登记实例、管理连接、能力与供应前置条件 | 持有租户集合、保存所有权表 |
| Database | 描述外部数据库、管理范围、绑定与回收策略 | 充当第二套 registry 或通用资源框架 |
| Tenant | 声明需求或显式选择资源,申请使用并交付凭据 | 删除时隐式销毁独立资源记录 |
Instance 与 Database 为 cluster-scoped,Tenant 为 namespaced。Database 的工作名称是
PostgreSQLDatabase;字段拼写与导入授权细节待 API 评审。
Database 由平台管理员管理,不属于应用 namespace,不引入资源专用 namespace。
Tenant 按名称引用 Database;Database 绑定记录包含 Tenant 的 namespace/name/UID。
普通申请者通过 Tenant 申请使用,不能自行修改 Database 回收策略或将 Released 资源重新开放。
2026-09-25 确认:第一版以一个 database、一个兼任 owner 的 login role 及其应用凭据
作为 Database 的生命周期边界,Tenant 负责申请与交付,不单独拥有账号或凭据生命周期。
不预留多账号字段,不新增独立 Role、Credential 或 Claim CRD。一库多账号若出现实际需求,
通过后续 API 版本演进处理,不纳入 v1alpha1。此边界不扩大导入资源的管理授权。
Database 自身必须声明 `instanceRef`,手工登记时同时指定实际数据库名;无需先存在 Tenant,
即可通过 Instance 验证目标。动态申请由 Tenant 选择 Instance,供应时把该引用写入 Database;
选择已有 Database 的 Tenant 从资源获取 Instance,不重复指定另一份来源。资源与实例的归属
独立于使用绑定,Tenant 删除后仍保留;修改引用不是数据库迁移。
参考 [Kubernetes PV/PVC](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) 的
资源/申请分离与绑定生命周期,不复制存储调度和 CSI 协议。资源与申请的关系不是 GC 所有关系。
## 3. 身份与事实来源
- Kubernetes spec 保存声明;受保护的资源绑定记录与 status 保存身份关联、操作进度和观察。
它们通过 API 持久化,不因 controller 重启而消失。
- PostgreSQL catalog 是 database、role、grant、extension 实际状态的来源。
- OpenBao 是应用凭据的事实来源;ESO 状态和目标 Secret 存在性说明投射结果。
- Instance、Database、Tenant 以 UID 区分对象身份;namespace/name 用于定位,
同名新 UID 不继承旧绑定。数据库 OID 仅供诊断,不是永久身份或删除授权。
- 不新增 PostgreSQL 所有权表、安装身份表或 Retain 墓碑;COMMENT 也不能授权认领。
- 记录操作意图不等于外部操作成功,phase 不等于外部所有权;执行前后仍须观察实际状态。
## 4. Instance 合同
Instance 声明 host、hostaddr、port(默认 5432)、管理 database(默认 postgres)、
TLS mode(默认 verify-full)及 controller namespace 的管理 Secret 名称和字段映射。
禁止隐式 TLS 降级。凭据引用不接受自选 namespace 或 OpenBao path。
管理员维护 ExternalSecret,由 ESO 同步管理 Secret;controller 只读,不修改管理密码,
不直接从 Bao 取管理凭据。有效用户名或密码变化时释放旧连接并重验;仅 metadata 或无关
字段变化不重建。中途凭据变化必须丢弃旧观察。已有有效管理凭据时,Bao/ESO 故障本身
不撤销 Instance Ready;首次缺少有效 Secret 时不能 Ready。
Ready 要求当前目标的连接、服务器 metadata 和所需管理能力检查通过,不要求创建、
迁移或读取 registry,也不证明备份或高可用。阶段简化为 Pending → Validating → Ready,
删除进入 Deleting。实际扩展可用列表不等于安装权限;查询失败不等于不支持。
endpoint 变更由管理员负责评估,不验证物理服务器连续性,不迁移或清理旧目标;
旧观察失效。新 UID 不接管旧资源。Instance 开始受管前保存 finalizer;删除时停止新供应,
只要有引用它的 Database(包括 Released/删除中)或尚未绑定的 Tenant 就等待。
查询失败不视为无引用;无引用才解除 finalizer,不级联删除任何业务资源。
引用检查不是跨对象事务,正在删除或不存在的 Instance 不允许开始新的供应/绑定。
## 5. 动态供应与排他绑定
1. 校验 Tenant 请求、Instance 能力、名称和扩展要求。
绑定 controller 在触及资源侧绑定前将 Tenant 进度记为 Binding,固定申请目标,
避免两次绑定写入之间修改引用占用第二个资源;该进度不是已绑定的声明。
2. 在首次外部写入前持久化独立 Database 记录、确定目标与管理范围。
动态创建的 Database 名称由 Tenant UID 确定;重试复用同一记录,不重复创建。
3. 先在 Database 写入 Tenant namespace/name/UID,再在 Tenant status 写入 Database
name/UID;双向记录一致后才允许供应或交付。
4. 按已确认步骤建立凭据、role、database、授权和扩展,逐步回读。
5. 验证应用登录与 ESO 投射后,Tenant 才可 Ready。
绑定前固定有效目标;绑定或开始外部供应后不得通过修改名称或引用实施隐式迁移。
每个 Database 最多一个使用者,每个 Tenant 最多一个 Database。
绑定 API 写入采用 resourceVersion 并发控制;双向记录不原子,单边完成不得授予使用权限。
采用 Kubernetes PV/PVC 的资源侧先写模式,参考
[官方 bind 实现](https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/volume/persistentvolume/pv_controller.go)。
Database 已绑定其他 Tenant 时报告 Conflict,不抢占;API 更新版本冲突时重新读取并判断,
不能盲目覆盖。资源侧成功而 Tenant status 写入失败时,下一次 reconcile 核对双方身份后
补写,不因单次失败撤销资源侧绑定。普通 controller 重启沿用这些持久记录继续协调。
这只处理 Kubernetes 绑定记录的部分完成,不提供外部数据库不确定创建结果的自动认领。
绑定成功不代表 Ready,具体字段及并发、重启、单边写入恢复必须由真实 API server 测试验证。
不同 Database 记录请求同一外部名称仍可能竞争,不能仅靠 Kubernetes 中的列表检查保证
PostgreSQL 名称唯一。后端创建时的重名失败报告 Conflict,失败方不得接管胜方资源。
不为此新增跨系统锁或 registry。管理员也不得把同一物理数据库登记成多个可绑定资源。
## 6. 显式导入
管理员创建资源声明,明确 Instance、已有数据库和允许管理的范围,构成导入授权。
初始检查只读验证存在性、owner、角色权限和扩展等是否匹配;不匹配报告清楚的差异,
不得通过重置密码、改变 owner 或撤销现有访问来“完成导入”。
未显式导入的同名数据库一律 Conflict。导入资源默认 Retain,不隐含 Delete 授权。
有权创建 Tenant 的申请者可以显式引用已登记、未绑定且可用的 Database;不增加资源侧
允许绑定名单或逐 Tenant 的管理员审批。绑定仍检查目标、可用状态与排他关系。
Released 不在可申请范围,必须由管理员处理旧访问并重新开放。导入时显式关联已有凭据,
不通过隐式改密生成替代凭据;具体关联字段在 API 中定义。
## 7. Retain、重新绑定与 Delete
回收策略属于 Database,默认 Retain;Tenant 删除是释放申请,不是独立资源的 GC 授权。
有资源管理权限的主体可在进入删除流程前修改 Retain/Delete;进入删除流程后策略固定。
显式设置 Delete 就是删除授权,不增加第二次审批或确认字段。
Database 不得设置会让它随 Tenant 消失的 ownerReference。
### Retain
- 保留 Database 对象、外部数据以及与资源关联的角色和凭据,不自动删除或重置。
- Tenant 释放后 Database 进入 Released,保留旧绑定身份用于诊断和防止自动复用。
Released 不是 Available,不再向原申请交付新状态,也不自动分配给同名新 Tenant。
- 保留策略不要求 PostgreSQL/Bao 在线才能完成申请释放,但必须先将释放关系安全记录到
Kubernetes;API 写入失败时不能宣称释放完成。仍有在途操作时不得跳过必要协调。
- 管理员检查数据、旧账号访问与凭据后,显式授权重新绑定。保留数据的复用可以不清空数据,
但必须由管理员确认新使用者应获得这些数据及旧使用者的权限处置。
- 删除旧投射 Secret 或解除绑定不等于撤销 PostgreSQL 访问;Retain 不承诺自动撤权。
ExternalSecret/Secret 的保留与清理细节需随凭据交付协议明确。
### Delete
必须由有权限的主体在资源侧明确授权,并核对 Database 身份、绑定、实际对象和管理范围。
在相关 finalizer 保护下清理投射、阻止新登录、处理已有连接、删除 database,再按已确认的
独占管理范围清理 role 与凭据;共享或未纳管的对象不得删除,禁止扩大 CASCADE 范围。
每步回读,失败保持进度与 finalizer;确认已删除的对象可幂等跳过,未知同名对象不能继续删除。
数据库被使用时,直接删除 Database 不得绕过绑定保护。资源已释放后才按策略处理。
具体 Tenant/Database finalizer 配合与投射清理顺序须经 API 设计及故障注入验收。
## 8. 幂等、失败与人工处理
普通依赖故障退避重试。已持久确认且仍与观察一致的步骤可以幂等继续;controller 重启
不重新生成密码,不重复创建已确认资源。
外部创建成功但记录尚未保存,或超时导致结果不确定时,若不能可靠确认归属,报告
Conflict 并停止相关写入;不得仅凭名称相同、曾记录意图或字段相似自动认领。
失败恢复不承诺全部自动完成,也不实现队列模拟事务。
status 缺失不假定发生于正常重启。Instance 可重新探测能力;Database/Tenant 缺少绑定或
操作确认时不能从外部同名对象推导所有权。按冲突/灾难恢复处理,不自动重建所有权表。
人工处理必须能看到:请求与资源身份、目标 Instance/database/role、失败步骤、已确认完成
与结果不确定的操作、冲突原因、下一步核实建议。保留现场,不自动删除疑似残留或改密。
管理员核实后可清理确定的残留再重试,或走显式导入;不能通过伪造 Ready/清空 status 强行继续。
## 9. 权限、凭据与扩展
2026-09-25 确认第一版管理账号使用原生非 superuser + CREATEDB/CREATEROLE 方案,
不引入 SECURITY DEFINER 接口;权限检查与限制见 [安全合同](security.md#最小权限)。
动态供应继续使用一个兼任 database owner 的 LOGIN role;应用角色不得具备 superuser、
CREATEDB、CREATEROLE 或 replication 权限。撤销 PUBLIC CONNECT,再授予目标角色;
不修改无关数据库和角色。identifier 匹配 `^[a-z][a-z0-9_]{0,62}$`,SQL 安全引用。
请求扩展按实例实际可用集合判断,成功后只追加,不自动 DROP EXTENSION。
可用列表查询失败按依赖错误处理;实际安装仍检查权限与结果。
应用密码使用安全随机源,只写 OpenBao;已有可信凭据可复用,不因失败生成第二份密码。
新建时先安全保存并回读凭据,再创建角色;凭据写入本身结果不确定也适用人工冲突规则。
Kubernetes 应用由 ESO 投射同 namespace Secret,controller 不直接写明文 Secret。
凭据仍输出 username/password/database/host/hostaddr/port/sslmode 七键,不生成带密码 URI。
Tenant status 提供 Secret 引用与无认证信息的 OpenBao API URL。
mount/base path 属部署配置,Tenant 不得自选任意路径;原按 Tenant namespace/name 固定
推导路径的规则撤除。动态供应的凭据路径按 Database UID 确定;导入时显式关联已有凭据
位置,不要求搬迁已有凭据。Released 不自动改密,管理员处理旧访问后才重新开放资源。
不得因换 Tenant、改部署参数或重新绑定就隐式搬迁凭据或改密。
2026-09-27 确认最小凭据记录:Database `status.credentialRef` 在首次外部写入前固定
mount/path;`status.credentialVersion` 仅在成功创建并回读后保存 KV 版本。位置和确认版本
不得自动更改或清空,Conditions 只描述当前可用性。已有值但无确认版本时报告 Conflict,
不能靠读取成功认领;已确认凭据消失或最新版本不一致同样停止,等待人工核实。
部署参数变化不得搬迁旧位置;确认记录写入的 resourceVersion 冲突不能通过盲目重试覆盖。
TLS、OpenBao Kubernetes auth、controller/ESO 身份隔离、Secret 读取范围和防泄漏要求
见 [安全模型](security.md)。这些安全约束继续适用。
## 10. Conditions 与可观测性
三类资源均提供唯一的 Ready Condition 及 observedGeneration,phase 只辅助表示阶段。
Ready=True 必须有当前目标的实际验证;Database 已绑定不等于 Tenant 凭据交付已经完成。
至少区分 Reconciling、InvalidSpec、ImmutableField、DependencyUnavailable、
AuthenticationFailed、InsufficientPrivileges、InstanceNotReady、Conflict、
ProvisioningFailed、CredentialProjectionFailed。Released 应明确显示未可供重新绑定,
具体 Condition Reason 由 API 细化,不假定仅靠 phase 判断授权。
使用结构化日志、Events 和低基数失败分类 metrics;Condition 是面向使用者的主要诊断入口。
禁止在任何 CR、Event、日志、metric、trace 或测试输出中出现密码、Token、完整秘密响应。
## 11. 验收
| 场景 | 必须验证的结果 |
| --- | --- |
| Instance 登记与重验 | 无 registry 依赖;真实凭据/TLS/管理权限检查 |
| 动态供应与重复 reconcile | 独立资源记录、排他绑定、密码不变、实际登录与投射成功 |
| 显式导入 | 无数据/密码/owner 隐式修改;错误目标、已占用或 Released 资源的申请被拒绝 |
| 同名未知资源 | Conflict,原数据库/角色/凭据不变 |
| 并发申请与单边绑定 | 最多一个使用者;失败方不能开始危险外部操作 |
| controller 重启 | 已确认步骤正常继续;不确定创建报告人工可诊断冲突 |
| Retain 与 namespace/Tenant 删除 | Database 不被 GC,外部数据保留,Released 不自动复用 |
| 人工重新绑定 | 旧 UID 不继承使用权;确认数据及凭据/旧访问处置后才能交付 |
| Delete 每步中断 | finalizer 保留,可重试,不误删未知/共享/未纳管对象 |
| 依赖稍后出现/权限恢复 | 安全重试,过期观察不授权写入 |
| status/备份恢复不一致 | 不凭同名推导归属,明确人工处理范围 |
| 泄漏与权限 | canary 不出现在输出;namespace 用户不能导入/回收他人资源 |
纯规则用单元测试;schema/CEL/status/watch/resourceVersion/绑定事件链用 envtest;
真实 PostgreSQL/OpenBao 验证后端行为;GC、ESO 与完整交付用具备相应控制器的测试集群。
envtest 不运行 GC/ESO,不能据此宣称这两类验收完成。详细测试与实现差距见
[开发文档](development.md)。导入不是数据迁移,dump/restore 仍是可选路径,见
[迁移文档](migration.md)。
@@ -1,4 +1,4 @@
# ADR-0001:采用 Kubernetes API 作为资源模型
# ADR-0001:采用 Kubernetes API machinery 作为状态协调平面
- 状态:Accepted
- 日期:2026-09-17
@@ -10,16 +10,49 @@ homelab 的基础设施状态分散在多套工具和后端中。仅集中 IaC
## 决策
Ayatori 使用 Kubernetes API machinery 与 CRD 表达平台资源、引用和状态,但不将平台
限定为容器编排系统。Controller 可以运行于专用 management environment,并管理集群外
的 VM、LB、数据库、对象存储、DNS、凭据和托管 Kubernetes 控制面。
Ayatori 使用 kube-apiserver、etcd、Kubernetes API machinery 与 CRD 构成 API 和状态协调
平面。主要复用的是以下难以可靠重建的能力:
- 版本化对象 API、schema、defaulting、validation 与 admission;
- 带 `resourceVersion` 的乐观并发、list/watch 与断线恢复;
- informer/cache/workqueue 生态;
- authentication、RBAC、namespace、审计与 API discovery;
- spec/status、conditions、finalizer 等控制面约定。
这项选择不把 Ayatori 限定为容器编排系统,也不意味着原生 Kubernetes workload API 是领域
模型。kube-apiserver 保存期望、引用和观察状态;Ayatori controller-manager 实现平台领域的
调度、生命周期、故障恢复、垃圾回收和后端收敛。Controller 可以运行于专用 management
environment,并管理集群外的 VM、LB、数据库、对象存储、DNS、凭据和托管 Kubernetes 控制面。
Ayatori 可以选择性复用 Kubernetes 内置资源的 API contract,而不采用其上游实现组件。例如,
`core/v1 Node` 可以表达计算节点身份、capacity、conditions、labels、taints 和维护状态,由
Ayatori Compute Agent 更新并由 Ayatori controller 消费;这不要求部署或模拟 kubelet,也不
要求存在 Pod、CRI、kube-scheduler 或 kube-controller-manager。`Lease`、`Namespace`、
`Secret`、`ConfigMap`、`Event` 和 RBAC 等资源同样按各自适用的 API 语义独立选择。
复用内置资源前必须明确其 producer、consumer、ownership、采用的字段和未采用的上游语义。
不能因为 Kubernetes 通常将若干组件一起部署,就把这些实现关系重新带入 Ayatori。
Kubernetes workload 集群与 OpenSandbox、Proxmox 等一样,是通过 adapter 接入的 backend 或
executor。它可以是远端集群,也可以完全不存在。除 Flux 和 Ayatori controllers 等管理组件的
部署外,领域 API 不得隐含依赖 controller 所在集群的 Pod、Job、Service、NetworkPolicy、
namespace 共置或 owner reference 语义;确有需要的能力必须由领域 API 和 adapter 契约显式表达。
GitOps 是长期期望状态的主要提交入口;API 是当前意图、关系和状态的在线控制面;真实后端
仍是运行事实来源。Controller 负责三者之间持续收敛。
`generic-apiserver` 或 Kubernetes API aggregation 只会让 Ayatori 接管资源的服务端实现,并不会
替代上述领域 controller。除非 CRD/kube-apiserver 的存储模型、API 语义或扩展边界形成经过验证的
阻碍,Ayatori 不自行承担 watch、RBAC、API 兼容、存储版本迁移和高可用 API Server 的实现与运维。
## 结果
- 获得统一声明式 API、watch、RBAC、admission、conditions 和 controller 生态。
- Ayatori controller-manager 实际承担类似 kube-controller-manager 的领域控制循环职责,必须把
reconcile、状态迁移、恢复与后端契约作为产品核心,而不是把它们误交给 kube-apiserver。
- 原生 Kubernetes workload 对象不能成为所有 adapter 的最低公共语义;Kubernetes 只是其中一种
执行后端。
- 允许由 Ayatori 自己实现合适的内置 API 资源语义;API 类型与上游 controller/runtime 不绑定。
- 可以把机器与人工执行统一建模为异步控制循环。
- 必须维护 CRD 版本、conversion、认证、备份和控制面升级。
- 不在 API 中保存日志、指标、大对象或业务数据,只保存控制所需状态及引用。
@@ -0,0 +1,109 @@
# ADR-0004:采用可拆分的模块化 Controller 架构
- 状态:Accepted
- 日期:2026-09-17
## 背景
Ayatori 将逐步提供任务执行、虚拟机、数据库、负载均衡、对象存储、托管 Kubernetes 和
人工操作等领域能力。这些能力拥有不同的生命周期、权限、网络位置和后端实现,若直接在
一个 controller 中相互调用并共享内部状态,后续接入新后端时容易形成代码耦合,也难以
独立扩缩容、发布和隔离故障。
另一方面,在首个领域能力完成前就拆分为多个独立服务,会立即引入镜像与部署管理、服务
间认证、版本兼容、分布式观测和故障处理成本,而这些成本尚未由真实运行需求证明。
Job 是首个领域对象。它既是最初的单次任务调度 API,也用于验证领域状态机与 Kubernetes
Job、OpenSandbox 和未来执行后端之间的适配边界。
## 决策
Ayatori 初期采用模块化单体:多个领域 controller 可以编译进同一个 controller manager,
但代码、API 所有权和依赖方向按照未来可独立部署的服务边界组织。
### 领域所有权
每个领域模块拥有自己的:
- CRD 与 API 版本;
- reconciler 和状态机;
- finalizer、conditions、删除及恢复语义;
- backend adapter contract;
- 领域测试。
初始领域包括但不限于 execution、compute、database、networking 和 human operations。领域
模块不得导入其他领域的内部实现,也不得直接修改其他领域所拥有对象的 spec 或 status。
### 跨领域协作
领域间的持久协作通过 Kubernetes API 对象、typed reference、owner reference 和
conditions 完成,而不是通过进程内 service 方法调用。
例如虚拟机完成创建后需要执行 provision,应创建或引用 Run 对象并观察其状态,而不是
直接调用 execution 模块的内部 Go API。更高层的资源组合由专门的领域对象或 GitOps 声明
完成,不引入统一包装所有底层能力的 Application controller。
该约束使 controller 即使暂时位于同一进程,其通信、失败和恢复行为仍与未来分进程部署
一致。
### Backend adapter
领域状态机只依赖本领域定义的最小 adapter contract,不依赖 Kubernetes Job、Crossplane、
OpenTofu、OpenSandbox 或具体厂商 SDK 类型。Adapter 负责:
- 幂等地确保期望外部资源存在;
- 观察并翻译外部状态;
- 执行取消、删除或 orphan 策略;
- 返回稳定的外部引用、能力和分类错误。
不同领域分别定义 adapter contract,不建立能包装所有资源类型的万能 Provider 接口。
后端不具备的能力必须显式报告,不通过虚假的统一语义隐藏差异。
Crossplane、OpenTofu 和 Ansible 等系统是可替换的 backend 实现或执行机制,不构成 Ayatori
面向用户的稳定 API。它们的 ProviderConfig、Workspace、playbook 等实现细节不得直接成为
领域 API 的必填契约。
### 共享代码
默认不建立跨领域的万能 service、repository 或 util 层。共享并非禁止,但必须来自已经
存在的真实重复,并同时满足:
1. 至少有两个真实调用者;
2. 重复的行为和语义一致,而不只是代码形状相似;
3. 调用方对其生命周期和预期演化方向一致;
4. 共享包不依赖任何具体领域的内部包。
适合共享的通常是机制,例如 conditions 操作、typed reference、重试退避、Secret 引用
读取、观测初始化和测试环境。领域状态机、资源策略、错误含义及后端选择不得为了消除少量
重复而抽取到共享层。
共享包使用表达具体职责的窄名称,初期保留在 `internal/shared/`。不建立内容持续膨胀的
通用 `util` 包,也不在实现稳定前承诺公共 Go API。
### 部署与拆分
controller manager 应支持按领域或 controller 集合选择性启用。初期可以使用一个二进制和
一个 Deployment;需要隔离时,优先使用同一制品部署为多个 Deployment。只有独立版本和
依赖关系成为真实需求后,才进一步拆分二进制或仓库。
出现下列任一情况时,应评估拆分部署:
- 需要独立扩缩容或显著不同的 reconcile 并发;
- 权限边界要求独立 ServiceAccount 与 RBAC;
- 后端只能从特定网络或节点访问;
- 沉重、不可信或冲突的 SDK 需要隔离;
- 一个领域的故障不应影响其他控制循环;
- 发布节奏或维护责任已经明确分离。
拆分不得改变领域 API,也不应将原本通过 API 对象完成的协作改为同步 RPC 链路。
## 结果
- 首个 Job 实现需要同时建立 execution 状态机和 adapter 边界,不能把 Kubernetes Job
细节写入领域模型。
- 初期避免承担不必要的微服务运维成本,同时保留按权限、网络位置和故障域拆分的路径。
- Kubernetes API 成为领域间异步协作和恢复边界,领域 controller 必须正确处理最终一致性。
- 部分机械代码会有意保留重复,直到共享语义被至少两个真实实现证明。
- 代码评审需要检查跨领域 import、对象写入所有权和后端类型泄漏。
- 当 Job 同时拥有 Kubernetes Job 与 OpenSandbox adapter 后,应复审 adapter contract,确认
它来自真实后端差异而非单一实现假设。
@@ -0,0 +1,98 @@
# ADR-0005:Job 使用短生命周期控制对象与 TTL 回收
- 状态:Accepted
- 日期:2026-09-17
## 背景
Ayatori 的 `Job` 表达一次有限时长、有退出结果的执行。CI、基础设施 controller 和用户可能
持续创建大量 Job。若所有 Job CR、后端 Kubernetes Job、Pod、Event 和日志都长期保存在
Kubernetes 中,etcd、apiserver list/watch、controller cache、备份及恢复会持续承担历史
数据成本。
Kubernetes API 适合作为活动执行的在线控制与协调面,但不应在没有明确产品需求时兼任无限
增长的执行历史数据库。当前也尚未证明 Ayatori 必须独立于调用者提供长期历史或审计检索。
Gitea Actions 等调用方已经拥有自己的执行历史;基础设施 controller 也应把 Job 结果转化为
所属领域资源的 status。执行日志属于观测数据,应进入统一 observability 平台,而不是由
Job CR 或专用执行历史存储重复保存。
Kubernetes 原生 `batch/v1 Job` 使用独立的 TTL-after-finished controller 回收完成对象。
该 controller 只支持原生 Job,不能直接处理 Ayatori CRD,但其基于 informer 与延迟工作队列
的实现模式可以复用。
## 决策
Ayatori 提供 `execution.ayatori.ddupan.top` API group 下的 `Job` Kind。与 Kubernetes
`batch/v1 Job` 同名不构成冲突,完整 GVK 明确资源身份。
Ayatori Job 是短生命周期控制对象,不是永久执行记录。首版不要求将结果归档到 PostgreSQL,
也不引入 `Archived` condition 或 `JobRecord` CRD。
### 终态与 TTL
Job 到达成功、失败或取消终态后保留有限时间,随后由 Ayatori 自己的 TTL controller 删除。
API 提供与原生 Job 语义一致的 `spec.ttlSecondsAfterFinished`;未设置时是否允许无限保留由平台
准入策略决定,而不是隐式默认永久保存。
TTL 从 controller 写入的可信终态时间开始计算。TTL controller:
1. watch Job 的新增和更新;
2. 只处理已终止、设置 TTL 且尚未删除的对象;
3. 未到期时使用延迟工作队列在到期时间重新入队;
4. 到期时重新读取最新对象并复核 UID、终态和 TTL;
5. 使用 UID precondition 发起删除,防止删除同名重建对象;
6. 由 Job finalizer 完成实际执行后端与临时凭据清理。
Controller 重启后,informer 的初始 LIST 会重新触发现存对象的 reconcile 并重建内存中的延迟
任务,因此不以周期性全量扫描作为正确性基础。
### 结果消费
调用者必须在 TTL 窗口内观察 Job 终态,并把需要长期存在的业务事实写入自身状态。例如 VM
provision Job 成功后,VM controller 更新 `Provisioned` condition;之后删除 Job 不影响 VM
状态。CI 系统负责保存自身 workflow 历史。
Job status 只保存控制和短期诊断需要的结构化结果,不保存完整日志、大型输出或 artifact。
### 日志与 artifact
各 execution adapter 必须为执行实例注入稳定的关联信息,使 stdout/stderr 能由共享
observability 管道采集,并能够按 Ayatori Job 的 namespace、name 和 UID 查询。Job status
可以保存查询观测数据所需的关联标识或受控链接,但日志内容及其索引、保留和查询能力属于
observability 平台。
Job 与后端执行对象的 TTL 应为日志采集提供合理窗口,但 GC 不以日志归档成功作为前置条件,
避免观测平台故障阻塞控制面资源回收。日志采集延迟、丢失和后端不可用通过 observability
自身的监控和告警处理。
Artifact 与日志语义不同。调用者需要消费的构建产物、状态文件或结构化输出必须显式写入
对象存储等持久后端,并通过引用交付;不能依赖日志系统作为 artifact 存储。
### GitOps 边界
一次性 Job 不由 Flux 持续管理。否则 TTL 删除会被视为漂移并重新创建,从而重复执行。GitOps
可以管理 Job template、schedule、execution class 和策略;CI、CLI、UI 或其他 controller
通过 Kubernetes API 命令式创建 Job。
### 未来归档
只有出现长期历史查询、统一审计、调用者无法及时消费结果等真实需求时,才引入
外部 `JobRecord`/History API。届时可以为需要持久化的 retention policy 增加归档流程,并将
归档成功作为删除前置条件;不要求所有 Job 无条件承担该成本。
## 结果
- etcd 中只保留活动 Job、短期已完成 Job 和需要人工处理的异常 Job。
- 首版不依赖 PostgreSQL 和对象存储即可完成 Job 纵向切片。
- 调用者必须正确 watch 或轮询结果;TTL 配置必须为其提供足够消费窗口。
- 历史日志由共享 observability 平台查询,Job CR 只提供执行关联信息。
- Job 删除后的历史默认不可从 Kubernetes API 恢复,这是有意接受的语义。
- TTL controller 是 execution 领域的一部分,可以和其他 controller 编译、部署在同一个
controller manager 中。
- 若未来增加归档,应作为独立产品能力和 retention policy 演进,不改变 Job 作为短生命周期
控制对象的基本定位。
## 参考
- [Kubernetes Automatic Cleanup for Finished Jobs](https://kubernetes.io/docs/concepts/workloads/controllers/ttlafterfinished/)
- [Kubernetes TTL-after-finished controller](https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/ttlafterfinished/ttlafterfinished_controller.go)
@@ -0,0 +1,45 @@
# ADR-0006:按实际管理缺口扩展资源 API
- 状态:Accepted
- 日期:2026-09-20
## 背景
Ayatori 可以在技术上逐步加入 VM、任务、数据库、负载均衡、对象存储、KaaS、FaaS 与应用
托管等能力。如果按传统私有云产品目录推进,项目会把后端“能够实现”的能力误当成 homelab
实际需要的产品,并承担没有消费者的 API、controller、升级和恢复成本。
当前真正反复出现的问题,是 Database、LoadBalancer 和 Bucket/Object Storage 缺少符合本环境
需求的稳定管理 API。Proxmox VM 也存在明确缺口:远程 API 能力有限,一部分操作只能登录节点
使用 CLI 完成,因此单靠 Terraform provider 或 Proxmox API 无法覆盖期望生命周期。
当前 `Job` controller 是验证 Kubernetes API machinery、状态机、finalizer、回收和 adapter 边界
的首个纵向切片。OpenSandbox 和 microVM 可以成为内部执行后端,但这不等于平台需要 Lambda、
Cloud Run 或其他 FaaS/PaaS 产品。
## 决策
Ayatori 不设置必须完成的云产品清单。新增北向资源必须由现实消费者、重复管理缺口和持续
reconcile 的明确收益驱动。
当前优先方向是:
1. `Database`;
2. `LoadBalancer`;
3. `Bucket` / Object Storage;
4. `VirtualMachine`,其价值已确认,但实现成本更高。
`Run`/当前实验性的 `Job` 定位为控制面执行原语和架构验证切片,不自动扩展为面向用户的计算
产品。KaaS 是可能有真实需求的候选能力,但不是必达终点。FaaS、Cloud Run 和应用托管默认不做,
除非未来以新的需求和 ADR 改变决定。
VirtualMachine controller 对外提供稳定北向 API;南向允许根据操作选择 Proxmox API、节点上的
受限强类型 Agent/CLI 或 `ManualTask`。节点 Agent 必须提供版本化、幂等、可观察和可审计的操作,
不能退化为任意远程 shell。
## 结果
- 路线图可以根据当前收益调整,不把技术可行性误作产品承诺。
- 第一个 Job controller 的实现仍有测试和架构验证价值,但其 API 不约束长期产品形态。
- VM 被保留为核心高价值方向,同时承认其南向集成不是单一 provider 能解决的问题。
- 每个新增资源都要独立证明生命周期和管理价值;已有 backend 不自动产生新的产品层。
@@ -0,0 +1,61 @@
# ADR-0007:复用 Node API 建立按需实现的 Compute 能力
- 状态:Accepted
- 日期:2026-09-20
- 实施优先级:Deferred;当前优先 Database、LoadBalancer 与 Bucket
## 背景
Ayatori 长期可能需要管理现有 Proxmox VM、当前 libvirt VM,以及允许普通计算节点临时加入、
排空和退出。Proxmox 的远程 API 不能覆盖全部所需操作;若 Ayatori 进一步实现节点 inventory、
简单 placement、fencing 和安全 reschedule,Proxmox 的控制面价值会逐步被替代。
同一物理节点未来也可能运行 OpenSandbox/Kata 等执行后端。Kata 虽然以 microVM 隔离 Pod 或
container,但其公开生命周期是 Sandbox/Run,不是具有磁盘、NIC、console、placement、迁移和
长期身份的 VirtualMachine 产品。
## 决策
### 节点 API
Ayatori 选择性复用 `core/v1 Node` 与 `coordination.k8s.io/v1 Lease` 表达计算节点身份、能力、
容量、健康、维护状态与心跳。它们只是 API contract:由 Ayatori Compute Agent 写入,并由
Ayatori 自有 controller 消费。
这项选择不引入 kubelet、Pod、CRI、kube-scheduler 或 kube-controller-manager。Compute Agent
不是对 kubelet 的模拟或兼容实现,而是 Node API 在 Ayatori Compute 领域中的正式 producer。
每个 Node 必须带 Ayatori ownership label;Agent 只能更新自己的 Node/status 与 Lease。
初版 VirtualMachine 显式指定 Node。出现实际需求后,再由 Ayatori controller 基于 Node 的
Ready、unschedulable、taints、labels、capacity 和已有 allocation 实现小规模 filter/score。
具体资源分配不能依靠多个 controller 反复改写 `Node.status.allocatable`;需要并发预留时增加
独立 Allocation 资源或等价的原子分配记录。
### VM 数据面
长期主路径可以是普通 Linux Compute Node 上的 libvirt/QEMU,由受限的 Compute Agent 执行
版本化、强类型、幂等且可观察的 VM 操作。Agent 不提供任意远程 shell。
Proxmox 是 brownfield 迁移后端:初期用于 adopt 现有 VM,并继续提供当前已有的集群、存储、
备份与 HA 能力。若 Ayatori Compute 已经可靠覆盖所需 placement、fencing、存储可移植性和恢复
语义,可以逐步把 PVE 节点迁移为普通 Compute Node;不为维持虚假 backend 对等性承诺永久支持
所有 Proxmox 特性。
### HA 边界
自动 reschedule 必须满足:旧节点已经可靠 fenced,且 Volume 明确报告可在目标节点使用。
任一条件无法证明时,VM 进入 Blocked/ManualTask,不得冒险在第二个节点启动。首版允许完全
人工 placement 与恢复;不以通用 Placement、透明 live migration、多租户 SDN 或 Nova 兼容为目标。
### Sandbox 边界
OpenSandbox/Kata microVM 归属于 Run/Sandbox backend 的隔离实现,不创建 VirtualMachine 资源。
若未来 VM 与 Sandbox 共享物理节点,容量协调必须另行形成经过验证的设计;不能仅因两者底层
都使用 KVM 就合并其北向生命周期。
## 结果
- 复用成熟 Node/Lease API,而不继承 Kubernetes workload plane。
- Compute 能力可以按 homelab 所需规模实现,不必复制完整 Nova。
- PVE 帮助现有资源平滑迁移,但不是长期架构必须保留的一层。
- Compute 方向已记录,但不改变当前 Database、LoadBalancer、Bucket 的产品优先级。
@@ -0,0 +1,96 @@
# ADR-0008:将 PostgreSQL Tenant Operator 合并为 Ayatori Database 模块
- 状态:Accepted
- 日期:2026-09-20
2026-09-24 修订:[ADR-0009](0009-database-resource-and-claim.md) 已明确替代本文对 ownership
registry、任意 status 丢失自动恢复及原 Retain 合同的沿用要求。Database 合并归属、来源保护、
无旧部署兼容负担及其他仍适用的安全边界继续有效;以下保留当时迁移决策的历史背景。
## 背景
独立仓库 `postgresql-tenant-operator` 已经为 homelab 共享 PostgreSQL 设计了
`PostgreSQLInstance` 与 `PostgreSQLTenant` API,并包含批准的行为规格、领域值对象、状态机、
PostgreSQL ownership registry、OpenBao/External Secrets 边界、迁移与恢复文档及测试。
Database 是 Ayatori 当前最优先的真实管理缺口之一。继续把该 controller 作为独立产品,会重复
维护 manager、API machinery、发布、认证、可观测性和通用 controller 约定,也会使后续应用组合
必须跨两个控制平面理解状态。
截至 2026-09-20,源仓库已经合并 Instance 的 Endpoint、凭据引用、身份/版本、定义与观测目标
等值对象,以及扩展支持模型和最小生命周期/checkpoint。它们尚未接入实际运行链路。完整 Ready
判定、Kubernetes Secret 管理凭据与连接刷新、应用层/数据库 adapter/controller 接入、CRD 规格
对齐及集成验证仍未完成;Tenant 的创建、凭据交付与 Retain/Delete 生命周期也未落地。
现有运行链路仍是直接读取 OpenBao 管理凭据的旧实现,不能作为新设计已经可用的证据。源仓库
本地 `feature/instance-extension-observations` 还保留两个未提交文件,用于 Instance 接受扩展观测
及测试;该工作已暂停,不能作为已合并能力或迁移基线。部分生成的 CRD/API 代码也仍落后于批准
规范,因此迁移不能把当前工作树或全部脚手架原样复制到 Ayatori。
## 决策
PostgreSQL Tenant Operator 合并为 Ayatori 的 Database 领域模块。保留已经批准且仍适用的安全、
所有权、幂等与删除行为,不重新发明 database、role、credential 和 registry 语义。
当前没有可用发布版本、没有被该 operator 托管的 PostgreSQL 实例或 Tenant,也没有需要在线
转换的已部署 CR。因此此次合并不承担旧实现兼容性:旧运行链路可以直接撤销,不保留直接读取
OpenBao 管理凭据的路径,也不兼容落后于规范的旧 CRD、samples 或实现细节。
没有部署兼容负担不等于重新设计已经批准的产品合同。源项目的系统规格、API 语义、Instance 与
Tenant 领域模型、状态机、ownership registry、OpenBao/ExternalSecret 凭据交付、Retain/Delete、
恢复与测试设计整体作为 Ayatori Database 模块的规范基线。除 API group、项目归属和装配结构外,
迁移不得静默改变这些行为;确需改变时必须先单独修订规格并记录决定。
目标结构遵守 Ayatori 的模块化单体边界:
```text
api/database/v1alpha1/
internal/database/domain/
internal/database/controller/
internal/database/adapter/postgresql/
internal/database/adapter/openbao/
internal/database/adapter/externalsecrets/
docs/database/
```
最终目录可按 Kubebuilder 与现有模块约定微调,但 Database 不依赖 execution/Job 模块,也不把
PostgreSQL、OpenBao 或 External Secrets 客户端放入共享万能 service/repository 层。
Database API 直接重构为 Ayatori 统一结构:API group 使用
`database.ayatori.ddupan.top/v1alpha1`,Go package 使用 `api/database/v1alpha1`,controller、
domain 与 adapter 放入 Ayatori 对应 Database 模块。原 `database.ddupan.top/v1alpha1` 不保留
别名、conversion 或兼容入口。
迁移前逐项核对批准规格、领域模型与当前 Go types;冲突时以批准规格为准。代码质量通过重写
旧运行链路、清晰 application/adapter 边界和测试实现,不通过改变已批准行为获得。无需实现在线
CRD conversion 或数据迁移。
## 迁移方式
1. 以包含已合并 Instance 领域基础和 CI #14 的最新 `main` commit 作为 source reference;记录
commit,将完整批准规格与设计文档迁入 Ayatori Database 文档,并迁移领域模型和纯单元测试。
设计合同直接复用;旧运行代码不逐文件复制。
2. 保留源仓库暂停中的脏工作树,不移动、提交或复制两个 extension observation 文件。以后可以
先在源仓库形成独立 commit,或在 Ayatori 根据批准合同重新实现,但不得把未提交内容描述为来源。
3. 在 Ayatori multi-group 项目中用 Kubebuilder 注册 Database API,按批准规格迁移 types,重新
生成 `database.ayatori.ddupan.top` CRD、DeepCopy 与 RBAC;不直接复制旧生成文件或旧 `PROJECT`。
4. 删除旧运行链路假设,以 Ayatori 当前 Go、Kubernetes 与 controller-runtime 版本重新建立
application ports 和 adapter contract;先恢复 PostgreSQL registry/adapter contract tests。
5. 逐片实现 Instance observe、Kubernetes Secret 管理凭据与连接刷新、Tenant provisioning、
OpenBao、ExternalSecret、删除与恢复流程;
每片必须包含对应单元、envtest 和真实 PostgreSQL/OpenBao 集成测试。
6. Ayatori 中的 Database 模块达到原项目验收标准并完成迁移演练后,冻结旧仓库并将其 README
指向 Ayatori;不同时运行两个 controller 管理同一组 CR。
不通过一次性 unrelated-history merge 或整仓复制保留表面上的 Git 历史。旧仓库和 source commit
保留完整来源历史;Ayatori 迁移提交按可审阅行为切片记录 provenance。
## 结果
- Ayatori 获得第一个真实产品领域,而不是继续围绕实验性 Job 扩张。
- 已批准的 DBaaS 设计与测试投资得到保留。
- 单一 manager/release 不意味着领域耦合;Database 仍保持独立 package、adapter 和测试边界。
- 可以从已合并的领域基础开始迁移;旧运行链路和未提交 extension observation 不进入首个切片。
- 无部署兼容负担允许彻底重写旧运行链路,不为尚未使用的实现技术债保留兼容层;已批准设计合同
仍然有效。
- Database 使用 Ayatori 统一 API group 与目录结构,不为未投入使用的旧 group 保留入口。

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