Compare commits
28
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e590542b0b
|
||
|
|
3d91abe380
|
||
|
|
fb2c34ce46
|
||
|
|
505aeb8e50
|
||
|
|
c110aceb0f
|
||
|
|
341c095a97 | ||
|
|
cd0d3a70ae
|
||
|
|
984c0aee73 | ||
|
|
813b4341d1
|
||
|
|
5a7b38ad26
|
||
|
|
8ac6283573
|
||
|
|
33fb3ec972 | ||
|
|
86953cd91a
|
||
|
|
ddd717209e
|
||
|
|
2756803ba4
|
||
|
|
76752c8443
|
||
|
|
36138f835a
|
||
|
|
de8b7f9b04
|
||
|
|
e6b9980b3e
|
||
|
|
641db531a8
|
||
|
|
7b841d7dba
|
||
|
|
af001b6188 | ||
|
|
7a972afaa6
|
||
|
|
5bd0455940
|
||
|
|
4846ff2aba
|
||
|
|
7a47221280
|
||
|
|
f559ffebf9
|
||
|
|
fd643da0ca
|
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
name: Verify
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
/bin/
|
/bin/
|
||||||
/dist/
|
/dist/
|
||||||
/coverage/
|
/coverage/
|
||||||
|
/cover.out
|
||||||
|
|
||||||
# Local configuration and credentials
|
# Local configuration and credentials
|
||||||
.env
|
.env
|
||||||
|
|||||||
@@ -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$
|
||||||
@@ -3,8 +3,47 @@
|
|||||||
- 本仓库是 ddupan.top homelab 的内部基础设施控制平面,不以通用发行版为初期目标。
|
- 本仓库是 ddupan.top homelab 的内部基础设施控制平面,不以通用发行版为初期目标。
|
||||||
- 提交、文档和代码注释优先使用中文;公共 API 标识符和代码遵循对应语言惯例。
|
- 提交、文档和代码注释优先使用中文;公共 API 标识符和代码遵循对应语言惯例。
|
||||||
- 不要重新实现已有成熟后端的核心能力;新增实现前先确认能否通过稳定 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;应用应直接组合正交的平台资源。
|
- 不要引入统一包装所有能力的 Application CRD;应用应直接组合正交的平台资源。
|
||||||
|
- Proxmox VM 的北向管理不能假定单一 API 覆盖完整生命周期。允许按能力组合 Proxmox API、节点
|
||||||
|
上的受限强类型 Agent/CLI 操作和 ManualTask;节点 Agent 不得退化为无版本契约的任意远程 shell。
|
||||||
- 所有 controller 必须考虑幂等、observe、finalizer、conditions、删除策略和恢复行为。
|
- 所有 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 及具体生产凭据不得提交到仓库。
|
- Secret、token、kubeconfig 及具体生产凭据不得提交到仓库。
|
||||||
- `deploy/dev/` 与 `deploy/prod/` 使用相同制品;生产版本只通过 promotion 更新。
|
- `deploy/dev/` 与 `deploy/prod/` 使用相同制品;生产版本只通过 promotion 更新。
|
||||||
- 内部专用不构成降低测试、版本、恢复、安全和可审计要求的理由。
|
- 内部专用不构成降低测试、版本、恢复、安全和可审计要求的理由。
|
||||||
|
|||||||
+33
@@ -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"]
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
# 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 容器验证凭据读取和连接更新。
|
||||||
|
KUBEBUILDER_ASSETS="$(shell "$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path)" go test -tags=integration -race -count=1 ./internal/database/...
|
||||||
|
|
||||||
|
.PHONY: lint-database-integration
|
||||||
|
lint-database-integration: golangci-lint ## 检查集成测试构建标签下的 Database 代码。
|
||||||
|
"$(GOLANGCI_LINT)" run --build-tags=integration ./internal/database/...
|
||||||
|
|
||||||
|
.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
|
||||||
@@ -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"
|
||||||
@@ -28,11 +28,15 @@ Ayatori 是 `ddupan.top` homelab 的内部基础设施控制平面。它以 Kube
|
|||||||
- [执行模型](docs/concepts/execution-model.md)
|
- [执行模型](docs/concepts/execution-model.md)
|
||||||
- [环境与发布](docs/concepts/environments.md)
|
- [环境与发布](docs/concepts/environments.md)
|
||||||
- [路线图](docs/roadmap.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-0002:采用 k0s 与可选工作负载运行时](docs/decisions/0002-k0s-optional-workload-runtime.md)
|
||||||
- [ADR-0003:直接连接 Dev API 的开发循环](docs/decisions/0003-dev-api-development-loop.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)
|
||||||
|
|
||||||
## 当前状态
|
## 当前状态
|
||||||
|
|
||||||
Ayatori 处于设计与早期实现阶段。第一个纵向切片计划是统一 Job API 与 Kubernetes
|
Ayatori 处于设计与早期实现阶段。当前使用 Job controller 验证第一个完整控制循环与 adapter
|
||||||
Pod executor,随后接入 OpenSandbox executor。
|
边界;它不是通用 Job Service 或 FaaS 产品承诺。首批实际产品方向是 Database、LoadBalancer
|
||||||
|
和 Bucket/Object Storage,具体顺序按纵向价值决定。
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
+184
@@ -0,0 +1,184 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"flag"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
|
||||||
|
// to ensure that exec-entrypoint and run can make use of them.
|
||||||
|
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
|
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
|
||||||
|
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||||
|
|
||||||
|
executionv1alpha1 "git.ddupan.top/panxiao81/ayatori/api/execution/v1alpha1"
|
||||||
|
// +kubebuilder:scaffold:imports
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
scheme = runtime.NewScheme()
|
||||||
|
setupLog = ctrl.Log.WithName("setup")
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||||
|
|
||||||
|
utilruntime.Must(executionv1alpha1.AddToScheme(scheme))
|
||||||
|
// +kubebuilder:scaffold:scheme
|
||||||
|
}
|
||||||
|
|
||||||
|
// nolint:gocyclo
|
||||||
|
func main() {
|
||||||
|
var metricsAddr string
|
||||||
|
var metricsCertPath, metricsCertName, metricsCertKey string
|
||||||
|
var webhookCertPath, webhookCertName, webhookCertKey string
|
||||||
|
var webhookPort int
|
||||||
|
var enableLeaderElection bool
|
||||||
|
var probeAddr string
|
||||||
|
var secureMetrics bool
|
||||||
|
var enableHTTP2 bool
|
||||||
|
var tlsOpts []func(*tls.Config)
|
||||||
|
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
|
||||||
|
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
|
||||||
|
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
|
||||||
|
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
|
||||||
|
"Enable leader election for controller manager. "+
|
||||||
|
"Enabling this will ensure there is only one active controller manager.")
|
||||||
|
flag.BoolVar(&secureMetrics, "metrics-secure", true,
|
||||||
|
"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
|
||||||
|
flag.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.")
|
||||||
|
flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.")
|
||||||
|
flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.")
|
||||||
|
flag.IntVar(&webhookPort, "webhook-port", 9443, "Port the webhook server listens on. "+
|
||||||
|
"Defaults to 9443. Set -1 to disable the webhook server.")
|
||||||
|
flag.StringVar(&metricsCertPath, "metrics-cert-path", "",
|
||||||
|
"The directory that contains the metrics server certificate.")
|
||||||
|
flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.")
|
||||||
|
flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.")
|
||||||
|
flag.BoolVar(&enableHTTP2, "enable-http2", false,
|
||||||
|
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
|
||||||
|
opts := zap.Options{
|
||||||
|
Development: true,
|
||||||
|
}
|
||||||
|
opts.BindFlags(flag.CommandLine)
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
||||||
|
|
||||||
|
// if the enable-http2 flag is false (the default), http/2 should be disabled
|
||||||
|
// due to its vulnerabilities. More specifically, disabling http/2 will
|
||||||
|
// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
|
||||||
|
// Rapid Reset CVEs. For more information see:
|
||||||
|
// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
|
||||||
|
// - https://github.com/advisories/GHSA-4374-p667-p6c8
|
||||||
|
disableHTTP2 := func(c *tls.Config) {
|
||||||
|
setupLog.Info("Disabling HTTP/2")
|
||||||
|
c.NextProtos = []string{"http/1.1"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !enableHTTP2 {
|
||||||
|
tlsOpts = append(tlsOpts, disableHTTP2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial webhook TLS options
|
||||||
|
webhookTLSOpts := tlsOpts
|
||||||
|
webhookServerOptions := webhook.Options{
|
||||||
|
TLSOpts: webhookTLSOpts,
|
||||||
|
Port: webhookPort,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(webhookCertPath) > 0 {
|
||||||
|
setupLog.Info("Initializing webhook certificate watcher using provided certificates",
|
||||||
|
"webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey)
|
||||||
|
|
||||||
|
webhookServerOptions.CertDir = webhookCertPath
|
||||||
|
webhookServerOptions.CertName = webhookCertName
|
||||||
|
webhookServerOptions.KeyName = webhookCertKey
|
||||||
|
}
|
||||||
|
|
||||||
|
webhookServer := webhook.NewServer(webhookServerOptions)
|
||||||
|
|
||||||
|
// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
|
||||||
|
// More info:
|
||||||
|
// - https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/metrics/server
|
||||||
|
// - https://book.kubebuilder.io/reference/metrics.html
|
||||||
|
metricsServerOptions := metricsserver.Options{
|
||||||
|
BindAddress: metricsAddr,
|
||||||
|
SecureServing: secureMetrics,
|
||||||
|
TLSOpts: tlsOpts,
|
||||||
|
}
|
||||||
|
|
||||||
|
if secureMetrics {
|
||||||
|
// FilterProvider is used to protect the metrics endpoint with authn/authz.
|
||||||
|
// These configurations ensure that only authorized users and service accounts
|
||||||
|
// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
|
||||||
|
// https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/metrics/filters#WithAuthenticationAndAuthorization
|
||||||
|
metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the certificate is not specified, controller-runtime will automatically
|
||||||
|
// generate self-signed certificates for the metrics server. While convenient for development and testing,
|
||||||
|
// this setup is not recommended for production.
|
||||||
|
//
|
||||||
|
// TODO(user): If you enable certManager, uncomment the following lines:
|
||||||
|
// - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates
|
||||||
|
// managed by cert-manager for the metrics server.
|
||||||
|
// - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification.
|
||||||
|
if len(metricsCertPath) > 0 {
|
||||||
|
setupLog.Info("Initializing metrics certificate watcher using provided certificates",
|
||||||
|
"metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey)
|
||||||
|
|
||||||
|
metricsServerOptions.CertDir = metricsCertPath
|
||||||
|
metricsServerOptions.CertName = metricsCertName
|
||||||
|
metricsServerOptions.KeyName = metricsCertKey
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
|
||||||
|
Scheme: scheme,
|
||||||
|
Metrics: metricsServerOptions,
|
||||||
|
WebhookServer: webhookServer,
|
||||||
|
HealthProbeBindAddress: probeAddr,
|
||||||
|
LeaderElection: enableLeaderElection,
|
||||||
|
LeaderElectionID: "a6325ed6.ddupan.top",
|
||||||
|
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
|
||||||
|
// when the Manager ends. This requires the binary to immediately end when the
|
||||||
|
// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
|
||||||
|
// speeds up voluntary leader transitions as the new leader don't have to wait
|
||||||
|
// LeaseDuration time first.
|
||||||
|
//
|
||||||
|
// In the default scaffold provided, the program ends immediately after
|
||||||
|
// the manager stops, so would be fine to enable this option. However,
|
||||||
|
// if you are doing or is intended to do any operation such as perform cleanups
|
||||||
|
// after the manager stops then its usage might be unsafe.
|
||||||
|
// LeaderElectionReleaseOnCancel: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
setupLog.Error(err, "Failed to start manager")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:scaffold:builder
|
||||||
|
|
||||||
|
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
||||||
|
setupLog.Error(err, "Failed to set up health check")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
|
||||||
|
setupLog.Error(err, "Failed to set up ready check")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
setupLog.Info("Starting manager")
|
||||||
|
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
|
||||||
|
setupLog.Error(err, "Failed to run manager")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# 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/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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
resources:
|
||||||
|
- manager.yaml
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
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
|
||||||
|
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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
resources:
|
||||||
|
- allow-metrics-traffic.yaml
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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,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
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
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
|
||||||
|
- 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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: metrics-reader
|
||||||
|
rules:
|
||||||
|
- nonResourceURLs:
|
||||||
|
- "/metrics"
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: ayatori
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
name: manager-role
|
||||||
|
rules:
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["pods"]
|
||||||
|
verbs: ["get", "list", "watch"]
|
||||||
@@ -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
|
||||||
@@ -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,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
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
## Append samples of your project ##
|
||||||
|
resources:
|
||||||
|
- execution_v1alpha1_job.yaml
|
||||||
|
- execution_v1alpha1_jobclass.yaml
|
||||||
|
- execution_v1alpha1_kubernetesexecutionparameters.yaml
|
||||||
|
- execution_v1alpha1_opensandboxexecutionparameters.yaml
|
||||||
|
# +kubebuilder:scaffold:manifestskustomizesamples
|
||||||
@@ -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)
|
||||||
@@ -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/)
|
||||||
@@ -4,9 +4,11 @@
|
|||||||
Git / CLI / Backstage
|
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
|
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 实例。两者可以
|
Dev 与 Prod 使用独立的 Kubernetes API、数据库、身份和 controller 实例。两者可以
|
||||||
@@ -34,16 +50,16 @@ Ayatori 不承载或重新实现数据面。控制面故障只应阻止创建与
|
|||||||
|
|
||||||
## 资源分层
|
## 资源分层
|
||||||
|
|
||||||
平台提供正交产品能力,例如:
|
平台只为已经验证的管理缺口提供正交产品能力。当前优先资源为:
|
||||||
|
|
||||||
- `Job`、`Sandbox`、`ManualTask`
|
|
||||||
- `VirtualMachine`
|
|
||||||
- `LoadBalancer`
|
- `LoadBalancer`
|
||||||
- `Database`
|
- `Database`
|
||||||
- `Bucket`
|
- `Bucket`
|
||||||
- `DNSRecord`
|
- `VirtualMachine`
|
||||||
- `Credential`
|
|
||||||
- `KubernetesCluster`
|
`Run`/当前实验性的 `Job`、`ManualTask` 等可以作为控制面执行原语,但不是因为底层能运行 OCI
|
||||||
|
image 就自动成为面向使用者的计算产品。`DNSRecord`、`Credential`、`KubernetesCluster` 等只在
|
||||||
|
出现独立生命周期和真实消费者后加入;尤其 KaaS 不是预定终点。
|
||||||
|
|
||||||
只有具备独立领域生命周期的能力才应成为高阶资源。应用本身通过 GitOps 组合上述资源,
|
只有具备独立领域生命周期的能力才应成为高阶资源。应用本身通过 GitOps 组合上述资源,
|
||||||
重复组合可通过模板或 Composition 表达,而不是扩展中央 Application API。
|
重复组合可通过模板或 Composition 表达,而不是扩展中央 Application API。
|
||||||
@@ -56,5 +72,17 @@ Ayatori 不承载或重新实现数据面。控制面故障只应阻止创建与
|
|||||||
2. 通过固定版本的 Terraform module 或 Ansible playbook 执行。
|
2. 通过固定版本的 Terraform module 或 Ansible playbook 执行。
|
||||||
3. 仅在必要时使用 GitOps bridge。
|
3. 仅在必要时使用 GitOps bridge。
|
||||||
|
|
||||||
|
Proxmox 是已知例外:其远程 API 不能覆盖所需的完整 VM 生命周期。VirtualMachine adapter 可以
|
||||||
|
按操作能力选择 Proxmox API、部署在节点上的受限强类型 Agent/CLI,或生成 `ManualTask`。Agent
|
||||||
|
必须提供版本化操作、幂等查询、operation ID 与审计,不能暴露任意 shell,也不能把 CLI 输出
|
||||||
|
直接当作长期稳定协议。
|
||||||
|
|
||||||
Controller 无论采用哪种执行方式,都必须提供一致的 ownership、conditions、删除语义、
|
Controller 无论采用哪种执行方式,都必须提供一致的 ownership、conditions、删除语义、
|
||||||
错误分类和恢复行为。
|
错误分类和恢复行为。
|
||||||
|
|
||||||
|
## API Server 边界
|
||||||
|
|
||||||
|
首选 kube-apiserver + CRD,持续复用其成熟的 watch、RBAC、版本化存储和 API 生态。
|
||||||
|
generic-apiserver 或聚合 API Server 不会减少领域 controller 的数量,只会把资源服务端、
|
||||||
|
兼容性和存储迁移责任转移给 Ayatori。只有 CRD 的限制已经形成可复现、不可通过合理领域建模
|
||||||
|
解决的阻碍时,才重新评估自建 API Server。
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# Database 模块
|
||||||
|
|
||||||
|
Database 是 Ayatori 首批实际产品领域之一。第一个迁移切片只建立 PostgreSQL Instance 的纯领域
|
||||||
|
模型,不注册 CRD、不启动 controller,也不访问 PostgreSQL、OpenBao 或 Kubernetes Secret。
|
||||||
|
|
||||||
|
## 来源基线
|
||||||
|
|
||||||
|
完整设计合同及首批领域模型与测试提取自原 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、目录和链接适配;其余已批准行为保持不变。
|
||||||
|
|
||||||
|
迁移只使用该 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 准备决策与完整回读、Ready 重验及本轮 evidence 前置检查;沿用已批准合同,不能把旧运行
|
||||||
|
链路接回该模型。各层验证边界见 [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 只释放本地资源。
|
||||||
|
|
||||||
|
当前只有 `ObserveVersion` 版本查询用例,不能产生完整 CapabilityObservation 或 Ready。
|
||||||
|
controller 接入、Secret watch、finalizer、registry 观测装配与真实权限检查仍待后续切片;并发 CR 更新
|
||||||
|
必须由调用者通过 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 的集成验收。
|
||||||
|
|
||||||
|
## Registry 所有权存储切片
|
||||||
|
|
||||||
|
`adapter/postgresql/registry` 直接迁入上述固定基线的 `internal/postgresql/registry`,
|
||||||
|
保留原 schema、版本表和唯一约束,不接回旧 controller。Store 使用调用方提供的 pgx 连接或
|
||||||
|
连接池,不创建连接池、不读取凭据、不管理重连。迁移复用 tern v2.4.3 的事务和 advisory lock,
|
||||||
|
所有权变更复用 PostgreSQL 唯一约束与行锁,没有新增迁移或锁框架。
|
||||||
|
|
||||||
|
Claim 支持相同归属的幂等重试;Retain 保留不可重新占用的墓碑;Delete 仅删除匹配归属的
|
||||||
|
managed 记录,且只应在调用方确认外部资源已删除后调用。Store 本身不删除 database、role
|
||||||
|
或凭据,也不授予 Tenant provisioning 权限。驱动错误仅供内部调用方分类,接入 controller 时
|
||||||
|
仍须按安全合同转换为脱敏的 Condition/Event,不能直接发布原始错误。
|
||||||
|
|
||||||
|
真实 PostgreSQL 测试覆盖重复/并发初始化、唯一约束冲突、并发 Claim、归属不匹配、Retain
|
||||||
|
与 Delete 重试、连接重建、取消恢复、未知 schema/超前版本拒绝,以及真实事务提交后注入
|
||||||
|
客户端失败的恢复分支。后者确定性模拟“提交成功但调用方不知道”,不声称覆盖真实网络分区。
|
||||||
|
registry 尚未装配到 InstanceService;初始化成功不能当作完整 registry 回读或 Instance Ready。
|
||||||
|
controller 重启、Secret watch、finalizer 与跨后端删除仍由后续 envtest/端到端切片验收。
|
||||||
|
|
||||||
|
## 设计入口
|
||||||
|
|
||||||
|
- [系统规格](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;
|
||||||
|
在对应实现切片完成前,不应把其中命令理解为当前仓库已经可执行的入口。
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
# v1alpha1 API 合同
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 状态 | Review |
|
||||||
|
| API group | `database.ayatori.ddupan.top` |
|
||||||
|
| version | `v1alpha1` |
|
||||||
|
| 最后更新 | 2026-09-10 |
|
||||||
|
|
||||||
|
本文把已批准的系统规格映射为 CRD 字段合同。API types、生成 CRD、sample 和测试必须与本文
|
||||||
|
一致。Ayatori 尚未注册这些 API,本页是后续实现的规范来源。
|
||||||
|
|
||||||
|
## 通用约定
|
||||||
|
|
||||||
|
- PostgreSQL identifier 匹配 `^[a-z][a-z0-9_]{0,62}$`。
|
||||||
|
- 所有引用名称使用 Kubernetes DNS label/name 的相应校验。
|
||||||
|
- Tenant 的 `spec.instanceRef` 与 `metadata.name` 长度合计不超过 241 个字符,确保派生的
|
||||||
|
`<instanceRef>-<metadata.name>-postgresql` 不超过 Kubernetes DNS subdomain 的
|
||||||
|
253 字符限制。
|
||||||
|
- port、TLS mode、deletion policy 等固定默认值由 CRD defaulting 提供。database、
|
||||||
|
loginRole、Secret 名称等依赖其他字段的值是 controller 语义默认值:字段保持省略,
|
||||||
|
controller 计算 effective value 并通过 status/受管资源展示,不引入 mutating webhook。
|
||||||
|
- 需要读取旧值或跨字段的校验由 CEL 或 controller 完成。
|
||||||
|
- `status` 由 controller 独占写入,禁止出现密码、Token、管理用户名或完整连接串。
|
||||||
|
- 两个 Kind 都只承诺一个 `Ready` Condition;调用方不得依赖内部协调阶段。
|
||||||
|
|
||||||
|
## PostgreSQLInstance
|
||||||
|
|
||||||
|
cluster-scoped,short name 为 `pginstance`。
|
||||||
|
|
||||||
|
### Spec
|
||||||
|
|
||||||
|
| 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 中的键名 |
|
||||||
|
|
||||||
|
`adminCredentialRef` 不接受 namespace 或 Bao path。管理 Secret 固定在 controller
|
||||||
|
namespace,名称须合法,两个字段须存在且非空。管理员维护 ExternalSecret,由 ESO
|
||||||
|
同步;controller 只读管理 Secret,不创建或修改它。此为 2026-09-13 批准的修订,
|
||||||
|
现有 API types、生成 CRD 和 samples 尚未更新。
|
||||||
|
|
||||||
|
Instance endpoint、管理凭据引用可以修改。修改后 controller 重新验证。
|
||||||
|
2026-09-14 修订:v1alpha1 不实现 allowedExtensions;现有 API types、生成 CRD 和
|
||||||
|
samples 中的字段待后续移除,不作为一个可配置但被忽略的策略保留。
|
||||||
|
扩展请求按目标 PostgreSQL 实际可安装列表判断,可用列表由应用层查询。
|
||||||
|
|
||||||
|
endpoint 由管理员负责,不校验变更前后是否同一物理服务器/registry,只重验新配置
|
||||||
|
的连接与管理能力。新 UID 按新 Instance 处理,不授权接管旧 UID 的 Tenant 资源。
|
||||||
|
|
||||||
|
### Status
|
||||||
|
|
||||||
|
| JSON path | 类型 | 含义 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `status.observedGeneration` | int64 | 最近完成有结论协调的 generation |
|
||||||
|
| `status.phase` | enum | `Pending`、`Validating`、`InitializingRegistry`、`Ready`、`Deleting` |
|
||||||
|
| `status.postgresqlVersion` | string | 从 server 回读的版本,不用于客户端解析 |
|
||||||
|
| `status.conditions[]` | `metav1.Condition` | 至少包含唯一的 `Ready` |
|
||||||
|
|
||||||
|
print columns:`Endpoint=.spec.endpoint.host`、`Phase`、`Ready`、`Age`。
|
||||||
|
|
||||||
|
Instance `Ready=True` 要求管理凭据可读、TLS/认证成功、server metadata 可读、registry
|
||||||
|
可访问且权限预检成功。它不代表数据库已经备份或高可用。
|
||||||
|
|
||||||
|
管理凭据从 Kubernetes Secret 装配;已有有效凭据可访问 PostgreSQL 时,Bao/ESO
|
||||||
|
暂时不可用不单独撤销 Instance Ready。Tenant 凭据操作仍依赖 Bao。
|
||||||
|
|
||||||
|
## PostgreSQLTenant
|
||||||
|
|
||||||
|
namespaced,short name 为 `pgtenant`。
|
||||||
|
|
||||||
|
### Spec
|
||||||
|
|
||||||
|
| JSON path | 类型 | 必填/默认 | 合同 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `spec.instanceRef` | string | 必填 | cluster-scoped Instance 名称 |
|
||||||
|
| `spec.database` | string | `metadata.name` | 合法 PostgreSQL identifier |
|
||||||
|
| `spec.loginRole` | string | `metadata.name` | database owner 兼应用 login |
|
||||||
|
| `spec.extensions` | set[string] | 空集合 | 必须属于目标实例实际可安装的扩展列表;成功创建后只允许追加 |
|
||||||
|
| `spec.credential.secretName` | string | `<instance>-<name>-postgresql` | 合法的同 namespace ESO target Secret 名称 |
|
||||||
|
| `spec.deletionPolicy` | enum | `Retain` | `Retain` 或 `Delete` |
|
||||||
|
|
||||||
|
Tenant 不声明 OpenBao mount 或 path。controller 使用部署级 mount/base path 和
|
||||||
|
`namespace/name` 推导稳定路径,并用 UID metadata 验证所有权。ExternalSecret 固定为
|
||||||
|
`<instanceRef>-<metadata.name>-postgresql`;目标 Secret 可以由用户指定,只需
|
||||||
|
满足 Kubernetes Secret 名称校验,不限制命名内容;省略时使用相同默认名。
|
||||||
|
|
||||||
|
`instanceRef`、`database`、`loginRole` 和 `credential.secretName` 在首次成功创建外部
|
||||||
|
状态后不可变。
|
||||||
|
`extensions` 只允许集合不变或追加;移除返回 `ImmutableField`,不会执行
|
||||||
|
`DROP EXTENSION`。`deletionPolicy` 在对象进入删除前可以修改;删除开始后以 finalizer
|
||||||
|
首次观察到的值为准,避免清理过程中改变授权范围。
|
||||||
|
|
||||||
|
### Status
|
||||||
|
|
||||||
|
| JSON path | 类型 | 含义 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `status.observedGeneration` | int64 | 最近完成有结论协调的 generation |
|
||||||
|
| `status.phase` | enum | controller 状态机的权威 checkpoint |
|
||||||
|
| `status.database` | string | 应用语义默认值后的实际 database 名称 |
|
||||||
|
| `status.loginRole` | string | 应用语义默认值后的实际 owner/login role 名称 |
|
||||||
|
| `status.databaseOID` | uint32 | 回读的 database OID,仅供诊断 |
|
||||||
|
| `status.credential.secretRef.name` | string | 同 namespace 目标 Secret 名称 |
|
||||||
|
| `status.credential.openBaoURL` | string | 完整 KV v2 API URL,不含认证信息 |
|
||||||
|
| `status.conditions[]` | `metav1.Condition` | 至少包含唯一的 `Ready` |
|
||||||
|
|
||||||
|
Secret reference 不重复 namespace,因为它必定与 Tenant 同 namespace。OpenBao URL 格式
|
||||||
|
为 `<consumer-address>/v1/<mount>/data/<derived-path>`;不得包含 Token、用户名、密码或
|
||||||
|
query credential。
|
||||||
|
|
||||||
|
Tenant phase 枚举为 `Pending`、`Planned`、`CredentialCreated`、`RoleCreated`、
|
||||||
|
`DatabaseCreated`、`ExternalSecretCreated`、`CredentialProjected`、`Ready`、`Deleting`。
|
||||||
|
它不包含 `Failed` 或 `Retained`;失败类型由 Condition Reason 表达。
|
||||||
|
|
||||||
|
print columns:`Instance`、`Database`、`Phase`、`Secret`、`Ready`、`Age`。完整 OpenBao URL 只在
|
||||||
|
YAML/JSON status 中输出。
|
||||||
|
|
||||||
|
两个 Kind 的 `status.phase` 都是 controller 状态机的权威 checkpoint。controller 用它
|
||||||
|
选择下一候选动作,但必须在动作前后核对外部事实,不能仅凭 phase 跳过幂等检查。status
|
||||||
|
丢失或领先于实际状态时必须保守重建/纠正。自动化就绪判断仍应读取 `Ready` Condition;
|
||||||
|
phase 用于进度展示、恢复和排障。
|
||||||
|
|
||||||
|
## Condition
|
||||||
|
|
||||||
|
每种类型最多一个 Condition;更新必须保留正确的 `lastTransitionTime` 语义。
|
||||||
|
|
||||||
|
| Reason | Kind | 可重试性 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Reconciling` | 两者 | 正常进行中 |
|
||||||
|
| `Ready` | 两者 | 已收敛 |
|
||||||
|
| `InvalidSpec` | 两者 | 修改 spec 前不会恢复 |
|
||||||
|
| `ImmutableField` | Tenant | 恢复原值或重新迁移 |
|
||||||
|
| `DependencyUnavailable` | 两者 | 自动重试 |
|
||||||
|
| `AuthenticationFailed` | Instance | 修复凭据/TLS 后重试 |
|
||||||
|
| `InsufficientPrivileges` | Instance | 修复管理 role 后重试 |
|
||||||
|
| `InstanceNotReady` | Tenant | Instance 恢复后重试 |
|
||||||
|
| `Conflict` | Tenant | 人工解除名称/所有权冲突 |
|
||||||
|
| `ProvisioningFailed` | Tenant | 按错误类别退避重试 |
|
||||||
|
| `CredentialProjectionFailed` | Tenant | ESO/Secret 恢复后重试 |
|
||||||
|
|
||||||
|
`Ready=True` 必须使用 Reason `Ready`。处理中为 `Unknown/Reconciling`;已知未满足合同为
|
||||||
|
`False`。Condition message 可以包含资源名和错误类别,禁止包含凭据值或完整 Secret。
|
||||||
|
|
||||||
|
## 删除语义
|
||||||
|
|
||||||
|
- `Retain` 不需要等待外部依赖;删除 CR 后外部记录保留原 UID 并标记 unmanaged。
|
||||||
|
- `Delete` 添加 finalizer,严格按规格的所有权验证和清理顺序执行;失败保持 finalizer。
|
||||||
|
- Instance 开始受管时即添加并保存 finalizer;删除时停止新供应,存在 Tenant 引用
|
||||||
|
(包括正在删除的 Tenant)就保留 finalizer,无引用才移除。引用查询失败时继续等待。
|
||||||
|
不级联删除 Tenant 或外部资源;管理员可使用运维逃生流程。
|
||||||
|
- finalizer 不禁止创建 Tenant CR;并发创建者遇到删除中或不存在的 Instance 不得
|
||||||
|
开始供应。首版不增加跨对象锁或准入控制,不承诺跨对象原子删除。
|
||||||
|
|
||||||
|
## 示例
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: database.ayatori.ddupan.top/v1alpha1
|
||||||
|
kind: PostgreSQLInstance
|
||||||
|
metadata:
|
||||||
|
name: shared
|
||||||
|
spec:
|
||||||
|
endpoint:
|
||||||
|
host: postgresql.home.arpa
|
||||||
|
hostaddr: 192.0.2.10
|
||||||
|
port: 5432
|
||||||
|
database: postgres
|
||||||
|
sslMode: verify-full
|
||||||
|
adminCredentialRef:
|
||||||
|
name: shared-postgresql-admin
|
||||||
|
---
|
||||||
|
apiVersion: database.ayatori.ddupan.top/v1alpha1
|
||||||
|
kind: PostgreSQLTenant
|
||||||
|
metadata:
|
||||||
|
name: netbox
|
||||||
|
namespace: netbox
|
||||||
|
spec:
|
||||||
|
instanceRef: shared
|
||||||
|
database: netbox
|
||||||
|
loginRole: netbox
|
||||||
|
extensions: [pg_trgm]
|
||||||
|
credential:
|
||||||
|
secretName: shared-netbox-database-credentials
|
||||||
|
deletionPolicy: Retain
|
||||||
|
```
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# 系统架构
|
||||||
|
|
||||||
|
本文是已批准 [`specification.md`](specification.md) 的架构视图。规范定义外部行为,
|
||||||
|
本文解释组件边界;二者冲突时以规范为准。Ayatori Database 模块目前只有首批领域模型,尚未
|
||||||
|
注册 API 或接入运行链路。
|
||||||
|
|
||||||
|
## 组件与数据流
|
||||||
|
|
||||||
|
```text
|
||||||
|
GitOps / kubectl / Terraform / Backstage
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Kubernetes API (CRD)
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Ayatori Database controller
|
||||||
|
| | |
|
||||||
|
v v v
|
||||||
|
PostgreSQL DBMS OpenBao KV ExternalSecret
|
||||||
|
catalog+registry |
|
||||||
|
v
|
||||||
|
Kubernetes Secret
|
||||||
|
```
|
||||||
|
|
||||||
|
- Kubernetes `spec` 保存期望状态;`status.phase` 保存 controller 状态机 checkpoint,
|
||||||
|
其他 status 字段保存可重建的观察结果。整个 status 都必须能由外部事实保守恢复。
|
||||||
|
- PostgreSQL catalog 保存 database、role、grant 和 extension 的实际状态。
|
||||||
|
- 两个 CR 的 `status.phase` 是 controller 状态机的权威 checkpoint。
|
||||||
|
- PostgreSQL 管理 database 中的 controller registry 只负责所有权、安装身份和保留标记。
|
||||||
|
- OpenBao KV v2 是应用凭据的事实来源。
|
||||||
|
- External Secrets Operator(ESO)读取 OpenBao,并创建应用使用的 Kubernetes Secret。
|
||||||
|
|
||||||
|
controller 不运行 PostgreSQL/OpenBao,不管理 VM、存储、备份或 OpenBao PKI,也不直接
|
||||||
|
把明文凭据写入 Kubernetes API。
|
||||||
|
|
||||||
|
## 资源模型
|
||||||
|
|
||||||
|
`PostgreSQLInstance` 是 cluster-scoped,由平台管理员创建,描述外部 PostgreSQL 的
|
||||||
|
DNS host、IP host address、端口、管理 database、TLS 模式和管理 Secret 引用。
|
||||||
|
实际可安装扩展由应用层查询后交给领域对象判定,v1alpha1 不实现管理员 allowlist。
|
||||||
|
|
||||||
|
管理连接使用管理员维护的 ExternalSecret 经 ESO 同步到 controller namespace 的
|
||||||
|
Secret;Instance 只选择 Secret 名称与字段,controller 只读,不直接从 Bao 获取
|
||||||
|
管理凭据。Tenant 凭据的创建、读取与销毁仍由 controller 直接访问 Bao。
|
||||||
|
|
||||||
|
`PostgreSQLTenant` 是 namespaced。一个 Tenant 对应一个 database、一个同时作为 owner
|
||||||
|
的 login role、一组只允许追加的 extension、一个由 controller 推导的 OpenBao KV
|
||||||
|
记录,以及同 namespace 的 ExternalSecret 和目标 Secret。
|
||||||
|
|
||||||
|
Tenant namespace 只提供 Kubernetes RBAC 和身份边界。database 与 role 名称在一个
|
||||||
|
Instance 内仍然全局唯一。
|
||||||
|
|
||||||
|
## Reconcile 与所有权
|
||||||
|
|
||||||
|
系统采用最终一致性,不在 Kubernetes、PostgreSQL、OpenBao 和 ESO 之间假装存在分布式
|
||||||
|
事务。每个外部写入前在 CR status 记录阶段,执行幂等操作,回读验证,再推进阶段:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Planned -> CredentialCreated -> RoleCreated -> DatabaseCreated
|
||||||
|
-> ExternalSecretCreated -> CredentialProjected -> Ready
|
||||||
|
```
|
||||||
|
|
||||||
|
controller 每轮同时读取 CR、registry、PostgreSQL catalog、OpenBao metadata 和 ESO
|
||||||
|
投射状态。`status.phase` 是状态机 checkpoint,但不能替代外部回读;丢失或与事实冲突
|
||||||
|
时必须保守重建/纠正。`metadata.generation` 只表示 spec 修改;Condition 的
|
||||||
|
`observedGeneration` 表示该版本是否已经完成一次有结论的协调。
|
||||||
|
|
||||||
|
所有权使用 Instance UID、Tenant UID 与 namespace/name 验证。database/role COMMENT
|
||||||
|
可以辅助排障,但不能代替 registry。未知资源只报告 `Conflict`,不得修改、接管或
|
||||||
|
删除。Retain 后用相同名称重建 CR 会获得新 UID,因此仍然冲突。
|
||||||
|
|
||||||
|
## 创建与删除边界
|
||||||
|
|
||||||
|
创建时先校验全部输入和冲突,再生成一次密码并写入 OpenBao,随后创建 role、database、
|
||||||
|
extension 和 ExternalSecret。只有 ESO 已投射 Secret 且应用凭据实际登录成功,Tenant
|
||||||
|
才可 Ready。
|
||||||
|
|
||||||
|
`Retain` 是默认删除策略,只移除 Kubernetes 管理关系并保留外部资源。显式 `Delete`
|
||||||
|
使用 finalizer,在重新验证所有权后依次删除 ExternalSecret/Secret、连接、database、
|
||||||
|
role、OpenBao KV 历史和 registry。详细恢复与逃生步骤见
|
||||||
|
[`operations.md`](operations.md)。
|
||||||
|
|
||||||
|
## 网络与 TLS
|
||||||
|
|
||||||
|
Instance 同时公布 DNS `host` 和 IP `hostaddr`。PostgreSQL server 证书必须包含对应的
|
||||||
|
DNS SAN 和 IP SAN,消费者自行选择可达目标,并可使用 `verify-full` 验证。OpenBao PKI
|
||||||
|
持有 CA 私钥并签发服务端证书;controller 只挂载公开 CA bundle。
|
||||||
|
|
||||||
|
OpenBao 的 controller 内部地址和外部消费者地址可以不同。Tenant status 同时提供目标
|
||||||
|
Kubernetes Secret reference 和不含认证信息的 OpenBao KV v2 API URL。
|
||||||
|
|
||||||
|
## 文档入口
|
||||||
|
|
||||||
|
- API 字段与 Condition:[`api-reference.md`](api-reference.md)
|
||||||
|
- 安装、依赖和配置:[`deployment.md`](deployment.md)
|
||||||
|
- 本地与 CI 测试:[`development.md`](development.md)
|
||||||
|
- 安全模型与最小权限:[`security.md`](security.md)
|
||||||
|
- 现有数据库迁移:[`migration.md`](migration.md)
|
||||||
|
- 日常排障和删除逃生:[`operations.md`](operations.md)
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# 部署与配置
|
||||||
|
|
||||||
|
> 本页迁入作为 Database 模块的目标部署合同。Ayatori manager flags、manifests 与发布装配尚未
|
||||||
|
> 实现;原设计行为保持有效,但当前仓库不能直接按本页完成部署。
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 状态 | Review |
|
||||||
|
| 环境 | homelab Kubernetes + 外部 PostgreSQL/OpenBao |
|
||||||
|
| 最后更新 | 2026-09-10 |
|
||||||
|
|
||||||
|
本文定义 v1alpha1 的运行依赖、启动顺序和部署级配置。当前 manifests 尚未实现这些
|
||||||
|
配置,示例是后续实现合同,不可直接用于现有脚手架。
|
||||||
|
|
||||||
|
## 依赖与顺序
|
||||||
|
|
||||||
|
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 配置合同
|
||||||
|
|
||||||
|
controller 使用以下 CLI flags。必填项缺失、路径无效或 duration 不为正数时,进程必须
|
||||||
|
在启动 manager 前失败;不得等到 reconcile 时才逐个资源报告配置错误。
|
||||||
|
|
||||||
|
| CLI flag | 必填/默认 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `--openbao-address` | 必填 | controller 可访问的 OpenBao API address |
|
||||||
|
| `--openbao-consumer-address` | 默认同 `--openbao-address` | 写入 Tenant status,必须能被预期外部消费者解析 |
|
||||||
|
| `--openbao-auth-mount` | `kubernetes` | Kubernetes auth mount 名称 |
|
||||||
|
| `--openbao-auth-role` | 必填 | controller ServiceAccount 对应 role |
|
||||||
|
| `--openbao-kv-mount` | `kv` | KV v2 mount;开发可显式用 `secret` |
|
||||||
|
| `--openbao-service-account-token-path` | `/var/run/secrets/kubernetes.io/serviceaccount/token` | Kubernetes auth 使用的投射 token 文件 |
|
||||||
|
| `--openbao-tenant-base-path` | 默认 `postgresql-tenants` | controller 专属 mount-relative 前缀 |
|
||||||
|
| `--external-secret-store-name` | 必填 | controller 创建的 ExternalSecret 固定引用 |
|
||||||
|
| `--postgresql-ca-bundle-path` | PostgreSQL TLS 模式必填 | 只读 PEM trust bundle,不含私钥 |
|
||||||
|
| `--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。
|
||||||
|
|
||||||
|
Tenant 路径固定推导为 `<base-path>/<namespace>/<metadata.name>`。namespace/name 都已通过
|
||||||
|
Kubernetes 名称校验,因此不再允许 CR 提供任意路径。KV v2 API URL 使用 consumer
|
||||||
|
address 拼为 `<address>/v1/<mount>/data/<base-path>/<namespace>/<metadata.name>`。
|
||||||
|
|
||||||
|
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、评估现有 Tenant,并走明确迁移;实现应把 mount/base path 视为安装
|
||||||
|
身份的一部分并在 registry 留存,以便检测错误配置。
|
||||||
|
|
||||||
|
## PostgreSQL 管理 role
|
||||||
|
|
||||||
|
生产部署禁止使用 superuser。管理 role 至少需要:
|
||||||
|
|
||||||
|
- 连接管理 database、读取必要 catalog;
|
||||||
|
- 创建/修改受管 login role;
|
||||||
|
- 创建 database 并指定 owner;
|
||||||
|
- 撤销 `PUBLIC` CONNECT、授予租户 role CONNECT;
|
||||||
|
- 连接租户 database 并创建实例实际支持、租户申请的 extension;
|
||||||
|
- 创建和维护 controller 专属 registry schema/table;
|
||||||
|
- `Delete` 时禁止连接、终止目标 database session、删除已验证归属的 database/role。
|
||||||
|
|
||||||
|
部分 PostgreSQL 操作天然要求较高权限,尤其终止其他 session 和安装某些 extension。
|
||||||
|
应优先使用 PostgreSQL 预定义角色、受控 SECURITY DEFINER 管理函数或限定数据库的
|
||||||
|
授权;任何不得不使用 superuser 的 extension 都必须按实例单独记录,不得扩大默认
|
||||||
|
controller 权限。最终可执行 SQL grant 将随 PostgreSQL adapter 集成测试固化。
|
||||||
|
|
||||||
|
## OpenBao 与 ESO
|
||||||
|
|
||||||
|
controller policy 仅允许在固定 tenant base path 下 create/read/update/delete KV v2
|
||||||
|
data 和 metadata,Delete 必须能永久删除全部版本及 metadata;不读取管理凭据路径。
|
||||||
|
|
||||||
|
管理凭据由管理员维护的 ExternalSecret 同步到 controller namespace;其 ESO 身份
|
||||||
|
只读对应管理路径,不能供 Tenant 使用。租户 ESO 身份只读 tenant base path,不得
|
||||||
|
读取 PostgreSQL 管理凭据。controller 不创建或修改管理 ExternalSecret/Secret。
|
||||||
|
`ClusterSecretStore` 由平台管理员创建,controller 只引用,不创建或修改 Store。
|
||||||
|
controller 创建的 ExternalSecret 与 Tenant 同 namespace,并设置 ownerReference;目标
|
||||||
|
Secret 包含固定七键:`username`、`password`、`database`、`host`、`hostaddr`、`port`、
|
||||||
|
`sslmode`。
|
||||||
|
|
||||||
|
## Kubernetes RBAC
|
||||||
|
|
||||||
|
- controller 可读/写 Instance、Tenant 的 status/finalizer 和 Event。
|
||||||
|
- 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 registry 和 OpenBao metadata,
|
||||||
|
先在隔离 Kind 环境运行 E2E。禁止在同一组 CR 上同时运行两个 controller 版本。若新版本
|
||||||
|
在执行任何破坏性迁移前失败,可回滚镜像;涉及 API/storage 或 registry schema 迁移时,
|
||||||
|
必须先写独立升级规格和回滚步骤。
|
||||||
|
|
||||||
|
## 上线验证
|
||||||
|
|
||||||
|
```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) 的备份/逃生检查。
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
# 开发与测试环境
|
||||||
|
|
||||||
|
> 本页迁入作为 Database 模块的测试分层与 fixture 合同。旧项目的 Make target、devcontainer
|
||||||
|
> 和脚手架版本尚未适配 Ayatori;实现时应复用 Ayatori 现有工具链,并保持这里定义的测试边界。
|
||||||
|
|
||||||
|
## Ayatori 已接入的凭据与 registry 切片测试
|
||||||
|
|
||||||
|
本节命令已在 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 不可达、权限和镜像拉取失败。
|
||||||
|
|
||||||
|
registry 测试独立使用一次性 PostgreSQL,不启动 Kubernetes API server。覆盖重复和并发迁移、
|
||||||
|
所有权唯一约束、并发占用、Retain 墓碑与 Delete 幂等、连接重建、取消恢复、未知 schema 与
|
||||||
|
超前版本拒绝。通过在真实 COMMIT 成功后注入客户端错误,验证结果不确定时的重试;这不替代
|
||||||
|
真实网络故障测试,也不覆盖跨后端的删除步骤。
|
||||||
|
|
||||||
|
这些测试尚不包含 Instance CRD/controller、Secret watch、status/finalizer 事件链、registry 观测装配、
|
||||||
|
权限探测矩阵、ESO 或 Tenant 供应。版本查询成功不意味着 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`、两个 CR 的
|
||||||
|
status 状态机、不可变字段、extension 只追加、registry 所有权和外部错误分类。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。registry 测试会删除并重建固定的测试 schema,因此禁止将该变量指向真实
|
||||||
|
homelab database。测试后运行 `make dev-down` 清理依赖。
|
||||||
|
|
||||||
|
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
|
||||||
|
-> 验证 registry、PostgreSQL catalog、OpenBao KV、ExternalSecret 和 Secret
|
||||||
|
-> 分别使用 DNS host 与 IP hostaddr 登录
|
||||||
|
-> 删除 Tenant 并分别验证 Retain 与 Delete(含故障点重试)
|
||||||
|
-> 删除 Kind
|
||||||
|
```
|
||||||
|
|
||||||
|
当前 controller 已实现第一条 Instance Ready 纵向链路:E2E fixture 在 Kind 内启动
|
||||||
|
PostgreSQL/OpenBao,配置 Kubernetes auth,验证管理凭据读取、PostgreSQL 登录、registry
|
||||||
|
migration 和 Instance Ready。Tenant provisioning、ESO、TLS DNS/IP SAN 与删除路径仍需
|
||||||
|
后续纵向切片覆盖,不能从 Instance Ready 推断这些合同已经通过。
|
||||||
|
|
||||||
|
## 测试数据与泄漏检查
|
||||||
|
|
||||||
|
- 只使用显眼的固定 canary 测试密码,测试后扫描日志、Event、Condition、metrics 和
|
||||||
|
CR dump,出现 canary 即失败。
|
||||||
|
- 每个最终一致性阶段都注入一次中断,重启后验证密码不变且阶段只向前推进。
|
||||||
|
- 清空、落后或伪造超前的 `status.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。
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
# Instance 领域对象规格
|
||||||
|
|
||||||
|
状态:Draft,含已确认决策。日期:2026-09-13。
|
||||||
|
|
||||||
|
上层合并边界见 [ADR-0008](../decisions/0008-merge-postgresql-tenant-operator.md)。本文只展开 Instance,不包含 Tenant 的供应
|
||||||
|
实现,也不新增 CRD 字段。设计签名用于评审职责与行为,不是待复制的 Go 接口代码。
|
||||||
|
|
||||||
|
## 1. 对象职责与生命周期
|
||||||
|
|
||||||
|
Instance 表示一次登记的 PostgreSQL 管理对象,是聚合根。它负责字段与策略校验、
|
||||||
|
根据观察结果判断能力是否满足要求、保护状态转换规则;不登录数据库,不读取 Bao,
|
||||||
|
不查询权限或初始化 registry。
|
||||||
|
|
||||||
|
本草案选择:**领域对象只接收数据并做业务决策,不直接或通过端口、回调访问外部。**
|
||||||
|
应用层调用适配器获取事实、执行被允许的操作,并将观察结果交回对象。Instance 不接收
|
||||||
|
context、客户端或 IO 接口。领域行为不是公共 SetReady:调用方提供事实,不能指定结论。
|
||||||
|
|
||||||
|
每轮从 CR 重建一个 Instance;对象不跨 reconcile 缓存,也不是线程共享单例。
|
||||||
|
管理连接可由装配层跨轮次复用,但连接复用不代表上次能力验证仍然成立。
|
||||||
|
|
||||||
|
身份与 endpoint 以管理员声明为准。改变 endpoint 不验证是否同一物理服务器或
|
||||||
|
registry,不增加安装身份连续性检查;只使旧观察失效,按新配置重验管理能力。
|
||||||
|
新 CR 是新 Instance,不自动获得旧 UID 资源的所有权,也不迁移或清理旧目标。
|
||||||
|
下文“观察绑定匹配”仅指结果属于本轮身份/配置,不是物理服务器身份认证协议。
|
||||||
|
|
||||||
|
## 2. 字段与值对象
|
||||||
|
|
||||||
|
所有可变状态封装在对象内部。构造后身份和本轮 definition 不可变;配置变更通过
|
||||||
|
下一轮装载新的 definition 处理,不提供任意 SetPhase/SetReady/SetEndpoint。
|
||||||
|
|
||||||
|
| 字段 | 类型与内容 | 来源/持久化 | 修改规则 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| identity | InstanceIdentity:UID、name | CR metadata | 本次对象身份内不可变;同名新 UID 是新对象 |
|
||||||
|
| revision | 正整数,期望配置版本 | metadata.generation | 本轮不可变;不是物理服务器版本 |
|
||||||
|
| definition.endpoint | Endpoint:host、hostaddr、port、managementDatabase、tlsMode | CR spec | 本轮不可变;新配置重验 |
|
||||||
|
| definition.adminCredential | CredentialReference:name、usernameKey、passwordKey | CR spec | 只引用 controller namespace 的管理 Secret,不存明文 |
|
||||||
|
| availableExtensions | 可选的实际可安装扩展集合 | 应用层从目标 PostgreSQL 查询;本轮观察,不新增 status 字段 | 未观察与已观察的空集合不同;目标变化后旧结果失效 |
|
||||||
|
| checkpoint | Pending/Validating/InitializingRegistry/Ready/Deleting | CR status.phase | 只能由领域动作变更,应用层负责持久化 |
|
||||||
|
| observedRevision | 最近完成有结论协调的版本 | CR status.observedGeneration | 成功或已知失败时更新,单纯记录意图不更新 |
|
||||||
|
| readiness | Unknown/Ready/NotReady,加安全失败类别和操作说明 | 由 status Ready Condition 重建,结果再映射回 Condition | 方法更新;不是第二套持久化状态 |
|
||||||
|
| reportedVersion | 可选服务器版本字符串 | status.postgresqlVersion;验证后从服务器更新 | 仅供展示,不能证明连接成功 |
|
||||||
|
| deleting | 是否已请求删除 | metadata.deletionTimestamp 映射 | 本轮不可变;优先于其他动作 |
|
||||||
|
| evidence | 可选 CapabilityEvidence | 本轮外部回读;不新增 status 字段 | 重建时始终为空,不能从 Ready Condition 伪造 |
|
||||||
|
|
||||||
|
Endpoint 的构造约束沿用 API:非空 host、合法 IP、1–65535 端口、合法 PostgreSQL
|
||||||
|
identifier、显式 TLS mode,禁止隐式降级。CredentialReference 包含合法 Secret 名称
|
||||||
|
及非空字段名,不包含 namespace 或 Bao path;namespace 由应用层固定为 controller
|
||||||
|
自身 namespace。这里校验领域值,不在对象里校验整个 controller 部署配置。
|
||||||
|
|
||||||
|
CapabilityEvidence 包含本轮目标绑定(Instance UID、revision、endpoint、凭据引用)、
|
||||||
|
server version、管理能力检查结果、registry 观察结果。registry 结果区分
|
||||||
|
Absent/NeedsMigration/Usable;连接失败不能当作 Absent。它不包含密码、token 或 DSN。
|
||||||
|
|
||||||
|
管理能力要求来自规格中的 role/database/grant/extension 操作,不等价于“能执行
|
||||||
|
SHOW server_version”。具体权限探测矩阵需在 PostgreSQL 适配器规格中定义,不能
|
||||||
|
让一个没有定义检查内容的布尔值承担验收。
|
||||||
|
|
||||||
|
不属于 Instance 的字段:Tenant 清单、客户端、连接池、token TTL、CA 文件句柄、
|
||||||
|
Kubernetes resourceVersion。resourceVersion 留在应用层作为乐观并发保存的前提。
|
||||||
|
|
||||||
|
### 扩展支持判定(2026-09-14 已确认方向)
|
||||||
|
|
||||||
|
v1alpha1 按目标 PostgreSQL 实际可安装的扩展列表判断请求,不实现管理员 allowlist。
|
||||||
|
allowlist 仅保留为后续可选策略,不接受一个看似生效、实际被忽略的策略字段;现有
|
||||||
|
CRD 的 allowedExtensions 应在对应 API 改动中移除,本次只修订文档。
|
||||||
|
|
||||||
|
应用层查询实际可用扩展并提供与本轮目标绑定的观察;Instance 只做集合判断,不
|
||||||
|
访问数据库。不沿用之前提议的字符正则,不自动改大小写或名称;SQL 适配器仍须
|
||||||
|
安全引用 identifier。可用列表不是已安装列表,也不保证权限或其他安装前提满足。
|
||||||
|
|
||||||
|
未观察/查询失败不得当作空集合或不支持;不得用旧目标的列表授权新目标的操作。
|
||||||
|
非空请求须属于已观察的可用集合,返回不支持的名称;空请求无需扩展支持判定,
|
||||||
|
但不绕过 Instance 的其他就绪要求。安装后仍需回读,不能以集合匹配代替安装验证。
|
||||||
|
列表变化不触发自动卸载;已有扩展的漂移处理留到 Tenant 用例细化。
|
||||||
|
|
||||||
|
## 3. 设计签名
|
||||||
|
|
||||||
|
```text
|
||||||
|
Reconstitute(identity, revision, definition, checkpointSnapshot, deleting)
|
||||||
|
-> Instance | InvalidDefinition
|
||||||
|
|
||||||
|
Instance.BeginValidation() -> Outcome
|
||||||
|
Instance.AssessManagement(observation: CapabilityObservation) -> Outcome
|
||||||
|
Instance.PlanRegistryPreparation(observation: CapabilityObservation)
|
||||||
|
-> AlreadyUsable | PreparationAllowed | PreparationDenied
|
||||||
|
Instance.AssessRegistryResult(result: RegistryPreparationResult) -> Outcome
|
||||||
|
Instance.AssessReadiness(observation: CapabilityObservation) -> Outcome
|
||||||
|
Instance.CheckExtensions(requested: ExtensionSet)
|
||||||
|
-> Accepted | ExtensionsUnsupported | ExtensionSupportUnobserved
|
||||||
|
Instance.RequireProvisioningReady() -> Accepted | InstanceNotReady
|
||||||
|
Instance.BeginDeletion() -> Outcome
|
||||||
|
Instance.Snapshot() -> InstanceSnapshot
|
||||||
|
```
|
||||||
|
|
||||||
|
Outcome 是正常推进、已知失败或方法前提不成立,不包含重试秒数、Kubernetes patch
|
||||||
|
或原始驱动错误。InstanceSnapshot 只包含 checkpoint、observedRevision、readiness、
|
||||||
|
reportedVersion,不能序列化 evidence。快照与集合访问返回值副本。
|
||||||
|
|
||||||
|
CapabilityObservation 是不可变的事实输入:目标绑定、服务器版本、管理能力检查项和
|
||||||
|
registry 观察结果;各检查项区分成功、失败、未观察,未观察不视为成功。失败只含安全
|
||||||
|
类别,不含驱动异常或凭据。对象校验目标绑定与当前身份/配置一致,拒绝不匹配输入,
|
||||||
|
不改变状态;完整性不足不能产生 Ready。观察结果由应用层收集,对象不能自行证明
|
||||||
|
这些事实的真实性或实时性;采集来源、同轮次关联和并发检查由应用层保证。
|
||||||
|
|
||||||
|
RegistryPreparationResult 为操作失败(目标绑定、安全失败类别)或操作后的完整回读
|
||||||
|
观察。单独的“迁移调用成功”不是就绪证据。CapabilityEvidence 是对象接受并判定满足
|
||||||
|
要求的观察值,不是调用方传入的 Ready 布尔值。
|
||||||
|
|
||||||
|
### 构造与恢复
|
||||||
|
|
||||||
|
Reconstitute 校验期望 definition;无效输入不构造一个可参与用例决策的 Instance。
|
||||||
|
入口把 InvalidDefinition 映射成 InvalidSpec,不必为了报告坏 CR 而制造非法领域对象。
|
||||||
|
checkpoint 缺失或未知时保守使用 Pending;reportedVersion 和 Ready 都只是旧观察,
|
||||||
|
evidence 为空。若 observedRevision 与 revision 不一致,旧 Ready 不得通过供应检查。
|
||||||
|
|
||||||
|
### 方法合同
|
||||||
|
|
||||||
|
| 方法 | 前置条件/输入 | 行为与状态变化 | 失败语义 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| BeginValidation | 未删除;初次登记、配置变更或需重建 checkpoint | 转 Validating,readiness=Unknown,清空 evidence;不做外部 IO,不推进 observedRevision | deleting 时不启动验证 |
|
||||||
|
| AssessManagement | 未删除;Validating;目标匹配的观察 | 判定管理访问、metadata、权限是否满足;registry 可用或可安全准备时转 InitializingRegistry,仍为 Unknown;不执行探测 | 失败保持 Validating,NotReady,observedRevision=当前版本 |
|
||||||
|
| PlanRegistryPreparation | 未删除;InitializingRegistry;本轮前置观察 | 根据管理能力及 registry 现状决定无需写入、允许准备或禁止准备;返回决策,不执行迁移、不标 Ready | 访问失败、不兼容或证据不足时禁止写入,NotReady;保持阶段,更新 observedRevision |
|
||||||
|
| AssessRegistryResult | 未删除;InitializingRegistry;准备结果或无需写入时的完整回读 | 按全部就绪条件判断回读结果;全满足才 Ready,并更新 observedRevision/version/evidence | 操作失败或回读不满足时保持 InitializingRegistry、NotReady;不得提前 Ready |
|
||||||
|
| AssessReadiness | 未删除;Ready;本轮观察 | 配置版本不一致时仅 BeginValidation;否则根据全部观察判断是否仍满足就绪条件 | 访问失败转 Validating/NotReady;registry 缺失或需迁移时转 InitializingRegistry,保存后下一轮修复 |
|
||||||
|
| CheckExtensions | 请求集合;本轮实际可用扩展观察 | 判断请求是否为实际可用集合的子集,返回不支持的名称;无 IO、无状态修改 | ExtensionsUnsupported 或 ExtensionSupportUnobserved;不卸载已存在扩展 |
|
||||||
|
| RequireProvisioningReady | 供 Tenant 用例使用 | 要求未删除、Ready、observedRevision 匹配,并有本次调用链的新鲜完整 evidence | 不满足即 InstanceNotReady;持久化 Ready 本身不构成授权 |
|
||||||
|
| BeginDeletion | deleting=true | 转 Deleting,清除供应能力,Unknown;不执行任何数据库或凭据删除 | 引用检查/finalizer 处理失败不得恢复成可供应 |
|
||||||
|
| Snapshot | 任意合法对象状态 | 返回可安全持久化的结果值 | 不触发 IO,也不改变状态 |
|
||||||
|
|
||||||
|
领域方法只检查对象状态,不知道 checkpoint 是否已落盘。“已持久化 checkpoint”是
|
||||||
|
应用用例执行外部写入的前提。内存字段变成 InitializingRegistry 不代表已保存成功;不能
|
||||||
|
在同一轮无条件接着执行迁移。通过用例测试验证此约束,而不是伪造一个内存事务。
|
||||||
|
|
||||||
|
AssessManagement 成功只是中间步骤,observedRevision 不前移;完成就绪判定或
|
||||||
|
明确失败才产生相应有结论结果。旧版本字符串可供诊断,但失败会清空 evidence。
|
||||||
|
|
||||||
|
Instance 不在本轮暴露 CreateDatabase/DeleteDatabase:Tenant 的供应/销毁授权来自
|
||||||
|
Tenant 和 OwnershipClaim,不是从 Instance.Ready 推导。数据库执行能力如何承接
|
||||||
|
已授权动作,留到 Tenant 对象规格,不在这里设计第二个万能 service。
|
||||||
|
|
||||||
|
## 4. 应用层与外部访问边界
|
||||||
|
|
||||||
|
```text
|
||||||
|
应用层依赖的适配器能力(不传入 Instance):
|
||||||
|
InspectManagement(context, target) -> ManagementObservation | AccessFailure
|
||||||
|
InspectRegistry(context, target) -> RegistryObservation | AccessFailure
|
||||||
|
EnsureRegistry(context, target) -> Completed | AccessFailure
|
||||||
|
```
|
||||||
|
|
||||||
|
应用层在 IO 前绑定目标并关联结果,领域对象在接受观察时检查身份和配置匹配;旧
|
||||||
|
endpoint 的成功结果不得用于新 endpoint。Inspect 是只读;EnsureRegistry 是幂等初始化/迁移,
|
||||||
|
不能顺带建立 Tenant 数据库或接管未知 schema。Completed 不足以推进 Ready,必须回读。
|
||||||
|
|
||||||
|
适配器由装配层绑定管理连接;Secret 读取与连接池释放留在该边界之后,Instance
|
||||||
|
管理连接不涉及 Bao token。适配器不得把基础设施异常转换成 Ready。失败区分依赖不可用、
|
||||||
|
认证失败、权限不足和 registry 不兼容;不兼容属于不可安全继续,不自动覆写。
|
||||||
|
registry 不兼容的具体 Condition 映射须在接口规格中确定,不能统一误报权限不足。
|
||||||
|
|
||||||
|
管理连接由应用层从 controller namespace 的 Secret 装配;管理员维护 ExternalSecret,
|
||||||
|
ESO 负责同步。Instance 路径不直接访问 Bao,也不以 Bao/ESO 当前可用性作为就绪条件。
|
||||||
|
首次装配缺少有效 Secret 时失败;已有凭据可正常访问 PG 时继续按 PG 能力判定。
|
||||||
|
检测到所引用 Secret 的有效用户名或密码变化时,应用/基础设施层使用新值重建连接池
|
||||||
|
并重新采集管理能力观察;metadata 或无关字段变化不重建。不要求 Instance generation
|
||||||
|
变化,也不能复用旧连接的成功观察来证明新凭据有效。Secret 变化监听、连接释放和
|
||||||
|
刷新均不进入领域对象;应用层保证旧连接观察不混入刷新后的调用链。
|
||||||
|
controller 不修改 PostgreSQL 密码、不回写 Secret 或 Bao 管理凭据。
|
||||||
|
|
||||||
|
## 5. 状态转换与初始化走查
|
||||||
|
|
||||||
|
```text
|
||||||
|
Pending --BeginValidation/保存--> Validating
|
||||||
|
Validating --AssessManagement(观察)/保存--> InitializingRegistry
|
||||||
|
InitializingRegistry --AssessRegistryResult(回读结果)/保存--> Ready
|
||||||
|
Ready --配置变化或访问失败/保存--> Validating
|
||||||
|
Ready --registry 需修复/保存--> InitializingRegistry
|
||||||
|
任意阶段 --删除请求/保存--> Deleting
|
||||||
|
```
|
||||||
|
|
||||||
|
1. 入口读取 CR,装配 definition、checkpointSnapshot;客户端不注入领域对象。
|
||||||
|
2. 应用层按 checkpoint 协调用例;首次调用 BeginValidation,没有 IO。
|
||||||
|
3. 保存 Validating。若保存失败,结束本轮,不执行 registry 写入。
|
||||||
|
4. 下一轮应用层调用适配器探测实例,将观察交给 AssessManagement;领域判定通过后
|
||||||
|
保存 InitializingRegistry,保存失败则停止,不进行迁移。
|
||||||
|
5. 再下一轮应用层采集前置观察,调用 PlanRegistryPreparation。仅在意图已持久化且
|
||||||
|
领域允许时调用 EnsureRegistry;AlreadyUsable 则跳过写入,PreparationDenied 则
|
||||||
|
保存失败结果并停止。允许的操作完成后回读,交给 AssessRegistryResult 决定能否
|
||||||
|
Ready;操作失败也用安全结果交回,不在应用层直接修改 phase。
|
||||||
|
6. 入口用原 resourceVersion 前提保存快照;并发变更导致冲突时重新装载,不覆盖新状态。
|
||||||
|
7. 后续 Ready 检查先由应用层探测,再调用 AssessReadiness;Tenant 用例同样获取当前事实,不能
|
||||||
|
仅凭另一个 CR 的 Ready Condition 永久缓存授权。实际资源写入仍须处理并发变化。
|
||||||
|
|
||||||
|
阶段调度和外部操作顺序在应用层;“观察是否满足业务要求、是否允许准备 registry、
|
||||||
|
哪些结果算完成、失败退到哪里”在 Instance 方法内。controller 不重复这些规则,
|
||||||
|
也不直接把 phase 设置成 Ready。领域允许操作并不锁住外部世界,适配器仍须保障幂等
|
||||||
|
和并发安全;禁止把旧观察当成永久授权。
|
||||||
|
|
||||||
|
## 6. 不变量与恢复验收
|
||||||
|
|
||||||
|
- UID 不随名称复用;不同 UID 的 evidence/结果不可互用。
|
||||||
|
- 未完成当前配置的能力回读,不能新产生 Ready,也不能通过供应检查。
|
||||||
|
- checkpoint 可以落后或被伪造;每次初始化/供应前都核对事实。status 清空只需重新
|
||||||
|
验证和幂等准备,不删除 registry,更不能重新生成 Tenant 密码。
|
||||||
|
- 迁移成功而 status 保存失败:重试回读已存在 registry,安全完成,不重复破坏性写入。
|
||||||
|
- registry 在 Ready 后消失:下一次回读撤销 Ready,保存修复意图后才能重新准备。
|
||||||
|
- 外部 IO 超时:产生安全失败结果;保存 status 使用仍有效的外层上下文,不能复用
|
||||||
|
已超时的 IO 上下文而丢失失败状态。
|
||||||
|
- 已请求删除的 Instance 不允许新供应;BeginDeletion 不删除 PostgreSQL、Tenant 或
|
||||||
|
Bao。应用层在开始受管时添加并保存 finalizer,而非出现 Tenant 后再添加。
|
||||||
|
删除时查询所有引用它的 Tenant(含删除中的对象);有引用或查询失败就保留
|
||||||
|
finalizer,确认无引用才移除。引用查询、finalizer 写入和本地连接释放均不属于
|
||||||
|
领域 IO,Instance 只根据删除请求禁用供应能力。
|
||||||
|
- 首版不为 Instance 删除增加跨对象锁或准入控制。并发创建的 Tenant CR 不被
|
||||||
|
finalizer 拦截,但遇到删除中/不存在的 Instance 不得开始供应;不承诺取消
|
||||||
|
已在途的外部操作,也不声称引用查询与移除 finalizer 是跨对象原子事务。
|
||||||
|
- CheckExtensions 失败不能授权扩展安装;可用列表变化不会自行卸载已有扩展。
|
||||||
|
- Snapshot、错误、日志和领域对象格式化不输出明文凭据或 token。
|
||||||
|
- 领域测试只提供观察值,无需数据库、网络、context 或 IO mock;相同状态和输入
|
||||||
|
得到相同决策。缺少检查项、目标不匹配和旧配置结果不得产生 Ready。
|
||||||
|
|
||||||
|
上述每条都对应领域或用例测试;真实权限检查、迁移与并发保障由适配器集成测试
|
||||||
|
验证。本文为设计文档,未执行或宣称通过这些测试。
|
||||||
|
|
||||||
|
## 7. 本轮待评审与后续阻塞项
|
||||||
|
|
||||||
|
本轮请先确认字段归属、应用层采集事实/Instance 纯决策的分工、方法与状态转换合同。
|
||||||
|
管理 Secret 来源、Bao 故障不单独撤销 Instance Ready,以及管理用户名/密码变化时
|
||||||
|
重建连接池,以及管理员声明的 Instance 身份/endpoint 和简化 finalizer 删除规则
|
||||||
|
均已确认。其他决策及未决项见总体草案,不增加后台清扫器或状态字段。
|
||||||
|
|
||||||
|
批准本对象结构不等于批准这些未决行为,也不意味着立刻实现完整供应链路。
|
||||||
|
|
||||||
|
## 8. 领域实现与验证边界(2026-09-21)
|
||||||
|
|
||||||
|
`internal/database/domain/instance` 按上述方法合同实现 Ready 纯判定。输入分别表达连接、
|
||||||
|
metadata、role、database、grant、extension 管理能力,以及 registry 的未观察、缺失、需迁移、
|
||||||
|
可用、不兼容和不可访问状态。检查零值或未知值按证据不足处理;操作失败只使用封闭的安全
|
||||||
|
类别,不接收驱动错误。`Snapshot.Failure` 是 Condition 映射的领域输入,不新增 CRD/status 字段。
|
||||||
|
|
||||||
|
管理能力分别指目标连接可用、服务器 metadata 可读,以及执行规格 §7 所要求的角色、数据库、
|
||||||
|
授权和扩展管理操作的能力;不是仅凭版本查询或扩展可用列表判定权限。具体 SQL 权限探测矩阵、
|
||||||
|
最小权限角色和扩展权限例外仍须在 PostgreSQL adapter 切片定义并用真实后端验证。
|
||||||
|
registry 不兼容独立保留为领域失败类别,不将其误报为权限不足;公开 Condition Reason 的映射
|
||||||
|
留待 API/application 切片按原合同评审。
|
||||||
|
|
||||||
|
领域测试验证完整回读、缺少检查项、状态重建、重复判定、目标不匹配、配置变化、依赖失败、
|
||||||
|
registry 丢失/不兼容、操作结果不确定和删除限制。只有本轮完整能力判定通过后,Instance
|
||||||
|
前置条件检查才通过;这不授予 Tenant 所有权,也不替代实际写入前的并发校验。
|
||||||
|
|
||||||
|
本切片不新增 controller、adapter、Secret 读取或外部生命周期操作。checkpoint 保存失败、
|
||||||
|
resourceVersion 冲突、watch 与 finalizer 事件链需由后续 application/envtest 验证;SQL 探测、
|
||||||
|
registry 初始化/迁移、超时后的真实状态回读和并发幂等由 PostgreSQL 集成测试验证;Secret
|
||||||
|
变化后的连接刷新由 Kubernetes API 加真实 PostgreSQL 的集成测试验证。纯领域测试不能证明
|
||||||
|
Database 已可运行或这些集成合同已完成。
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
# 领域模型设计草案
|
||||||
|
|
||||||
|
状态:Draft,含已确认决策。日期:2026-09-13。
|
||||||
|
|
||||||
|
本文定义领域职责、身份与一致性边界,并用对象规格细化字段和方法合同;方法使用
|
||||||
|
设计签名,不固定 Go 目录、SDK 或框架,也不批准实现。外部行为以
|
||||||
|
[系统规格](specification.md) 为准;下列未决问题不能由实现自行决定。
|
||||||
|
PR #6 的代码和已有 registry 表结构是可评估的实现素材,不反向决定领域模型。
|
||||||
|
|
||||||
|
## 1. 领域与统一语言
|
||||||
|
|
||||||
|
本系统的领域是“在共享 PostgreSQL 上供应并管理应用租户”,不是数据库服务器运维。
|
||||||
|
v1alpha1 先采用一个限界上下文,不把 PostgreSQL、Bao、Kubernetes 各自当成业务上下文。
|
||||||
|
|
||||||
|
| 术语 | 含义 | 不是什么 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Instance | 平台登记的外部 PostgreSQL 管理对象及其供应策略 | 连接池、VM 或 controller 单例 |
|
||||||
|
| Tenant | 一个应用的数据库使用合同及受管资源生命周期 | PostgreSQL database 的别名 |
|
||||||
|
| Database | 租户数据库的名称、owner、扩展等期望描述与实际观察 | 包含 Bao 登录与连接关闭的操作接口 |
|
||||||
|
| LoginRole | 同时作为 database owner 和应用登录身份的角色 | 额外的 NOLOGIN owner |
|
||||||
|
| OwnershipClaim | 某个 Tenant 身份对一组资源名称与凭据位置的所有权声明 | 工作流阶段或仅凭名称推断的归属 |
|
||||||
|
| CredentialLocation | 固定推导的凭据位置及所有权关联 | 密码本身或用户可任意选择的 KV path |
|
||||||
|
| CredentialProjection | 把既定凭据交付到目标 Secret 的要求与观察结果 | controller 直接写入明文 Secret |
|
||||||
|
|
||||||
|
UID 表示一次 Kubernetes 对象身份;namespace/name 用于定位,不足以证明归属。
|
||||||
|
database OID 是诊断观察值,不充当本系统的租户身份。
|
||||||
|
|
||||||
|
## 2. 候选聚合边界
|
||||||
|
|
||||||
|
### Instance:实例能力与供应策略
|
||||||
|
|
||||||
|
Instance 是候选聚合根,持有自身身份、endpoint、管理凭据引用、实际可用扩展观察,
|
||||||
|
以及用于判断当前能力的观察结果。它不持有所有 Tenant 对象的集合。
|
||||||
|
|
||||||
|
其行为包括:
|
||||||
|
|
||||||
|
- 判断租户申请的 extension 是否在本实例实际可安装列表中;v1alpha1 暂不实现 allowlist。
|
||||||
|
- 根据管理连接、服务器信息、registry 和权限检查结果判断是否具备供应能力。
|
||||||
|
- 判断配置变化使哪些能力观察过期,禁止以旧 generation 的 Ready 证明新配置可用。
|
||||||
|
- 在 registry 初始化完成并回读验证后,接受新的就绪结果。
|
||||||
|
|
||||||
|
“探测实例”“准备管理 registry”是应用用例协调的外部操作,不是 Instance 的 IO 方法。
|
||||||
|
领域对象只接收观察值,负责前提、规则和状态决策;应用层调用适配器获取事实与执行
|
||||||
|
获准操作。领域对象不持有或调用外部访问端口、客户端或回调。具体选择见
|
||||||
|
[Instance 字段与行为](domain-instance.md),仍处于待评审状态。
|
||||||
|
|
||||||
|
### Tenant:供应合同与资源生命周期
|
||||||
|
|
||||||
|
Tenant 是另一个候选聚合根,通过身份引用 Instance,而不是 Instance 的聚合成员。
|
||||||
|
操作一个 Tenant 不应要求装载、锁定或保存整个实例的租户集合。
|
||||||
|
|
||||||
|
Tenant 持有有效的 database/role 名称、请求的扩展、凭据交付目标、删除策略,以及
|
||||||
|
已建立的资源绑定。它负责:
|
||||||
|
|
||||||
|
- 检查绑定后的不可变字段、extension 只追加规则。
|
||||||
|
- 判断外部部分状态属于本 Tenant、尚不存在,还是与未知资源冲突。
|
||||||
|
- 决定是否允许继续供应、何时达到 Ready、是否允许释放受管资源。
|
||||||
|
- 按 Retain/Delete 合同限制行为,禁止把保留资源自动认领给同名新 UID。
|
||||||
|
|
||||||
|
Database、LoginRole 和 CredentialProjection 暂不设独立聚合根或独立 CRUD 用例。
|
||||||
|
它们可作为 Tenant 内的资源描述与观察值;有规则才增加行为,不为了“充血”添加方法。
|
||||||
|
真实 PostgreSQL database/role 的存在不意味着内存中必须各有一个有身份的实体。
|
||||||
|
|
||||||
|
聚合边界是业务规则的保护边界,不表示 Tenant 对应的 PostgreSQL、Bao、ESO 资源
|
||||||
|
能够一次事务提交。跨系统供应必须允许部分完成。
|
||||||
|
|
||||||
|
### OwnershipClaim:跨租户唯一性与持久证据
|
||||||
|
|
||||||
|
名称唯一性不可能只靠某个 Tenant 的内存检查保证。需要一项领域能力,在持久化边界
|
||||||
|
原子认领资源;已有 registry 是其适配器候选,仍需结合 catalog 和 Bao metadata 检查。
|
||||||
|
|
||||||
|
Claim 与 Tenant 关联,但不随 Tenant CR 消失:Retain 后证据必须继续存在。因此不能
|
||||||
|
把它仅视为 CR 的附属 status。是否作为独立的小聚合,先以“可独立持久化、保留并保护
|
||||||
|
归属不变量的声明”建模;不因此引入新的 CRD。
|
||||||
|
|
||||||
|
- 同一身份、同一绑定的重复认领可以成功;不同 UID 或不同绑定不能覆盖。
|
||||||
|
- Claim 预留名称不等于证明同名外部资源由本 controller 创建。
|
||||||
|
- 实际写入仍须核对所有权,不能把先查后建当成并发安全保证。
|
||||||
|
- 当前 registry 的数据库事务不能覆盖 Bao;跨实例的凭据路径竞争也不能靠单个
|
||||||
|
registry 的唯一约束解决。写入前提与条件创建协议需单独设计和验收。
|
||||||
|
|
||||||
|
## 3. 领域、用例与适配器的分工
|
||||||
|
|
||||||
|
| 层 | 承担的职责 | 禁止承揽的职责 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 领域对象/策略 | 身份、有效合同、归属判断、允许的动作、完成条件 | 外部 IO(包括通过接口间接调用)、解析 CLI、生成 Kubernetes Condition |
|
||||||
|
| 应用用例 | 装载模型与事实、持久化意图、调用能力、回读、提交结果 | 另写一套绕过领域规则的判断流程 |
|
||||||
|
| controller 入口 | CR 映射、调度、watch、重试、status/finalizer 写入 | 在 reconcile 中重新定义业务规则 |
|
||||||
|
| 基础设施适配器 | PostgreSQL、registry、Bao、ESO 的实际读写与并发保障 | 自行决定接管、改密码或扩大删除范围 |
|
||||||
|
| 启动装配 | 校验部署配置,创建共享客户端、连接管理器及用例依赖 | 把连接生命周期当成 Instance 的业务状态 |
|
||||||
|
|
||||||
|
领域可使用独立的身份、endpoint、identifier、extension 集合等值对象,不依赖 CRD
|
||||||
|
类型、pgx pool 或 Bao SDK。Kubernetes 对象的存取与 registry 的存取不是一个通用
|
||||||
|
`Save(Tenant)` 可以原子完成的事情;不虚构跨系统 Unit of Work。
|
||||||
|
|
||||||
|
暂不引入事件总线、事件溯源、通用聚合框架或全套 Repository CRUD。领域建模的依据
|
||||||
|
是业务规则,而不是接口和目录数量。
|
||||||
|
|
||||||
|
## 4. 状态与恢复
|
||||||
|
|
||||||
|
CR `status.phase` 仍是已批准的工作流 checkpoint,不在内存对象或 registry 再建一套
|
||||||
|
权威 phase。领域对象可以由 CR 的期望状态、checkpoint 和外部观察重新构造。
|
||||||
|
|
||||||
|
phase 只决定候选步骤,外部证据决定该步骤是否允许执行、是否已经完成。应用层在
|
||||||
|
写操作前保存意图,调用幂等操作后回读,再保存下一 checkpoint。status 写入失败时,
|
||||||
|
下次从外部事实识别完成结果;不能重发密码,也不能相信伪造的 Ready。
|
||||||
|
|
||||||
|
业务失败区分 InvalidSpec、ImmutableField、Conflict 等;依赖故障由适配器转换成
|
||||||
|
安全的能力失败,应用层决定重试并映射 Condition。凭据不进入模型序列化、status、
|
||||||
|
事件或错误明细;只能在实际需要它的执行边界短暂传递。
|
||||||
|
|
||||||
|
## 5. 用例走查与验收方向
|
||||||
|
|
||||||
|
| 场景 | 领域判定 | 应用与适配器执行/恢复 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 登记 Instance | 当前配置的能力要求是否满足 | 读取管理凭据,验证连接与权限,准备并回读 registry;完成后才 Ready |
|
||||||
|
| 供应 Tenant | Instance 策略、绑定与归属允许供应 | 保存意图,认领资源,先写并回读 Bao 凭据,再创建 role/database,登录验证和 ESO 投射 |
|
||||||
|
| Bao 写入后进程中断 | 同一身份的部分状态可继续 | 回读原凭据继续,不生成第二份密码 |
|
||||||
|
| 两个 Tenant 竞争名称 | 只有匹配所有权的一方可继续 | 持久化认领和条件写入裁决竞争,失败方 Conflict,不覆盖资源 |
|
||||||
|
| Delete 中断 | 已消失资源可视为完成;剩余资源仍须归属正确 | 按规格顺序继续删除,全部回读不存在后才清 registry 和 finalizer |
|
||||||
|
| Retain 后同名 CR 重建 | 新 UID 不等于原所有者 | Conflict,不恢复管理、不改密码 |
|
||||||
|
|
||||||
|
领域测试验证规则与决策;adapter 测试验证锁、条件写入、SQL 与协议行为;controller
|
||||||
|
测试验证 checkpoint 持久化和重启恢复;E2E 验证最终合同。不能只验证一串 mock 调用
|
||||||
|
就声称实现了最终一致性。
|
||||||
|
|
||||||
|
## 6. 决策记录与待细化边界
|
||||||
|
|
||||||
|
1. **Instance 身份与物理目标(已确认)**:以管理员声明为准,endpoint 变更不验证
|
||||||
|
物理服务器/registry 连续性,不增加安装身份绑定检查;旧观察失效,重验新配置
|
||||||
|
的连接与管理能力。新 CR 视为新 Instance,不自动接管旧 UID 资源或迁移数据。
|
||||||
|
2. **Retain 完成条件**:外部依赖不可用不能永久阻止 CR 删除,但 registry 又需标记
|
||||||
|
unmanaged。需定义 CR 消失后的补偿/清扫入口及所需身份依据,不能承诺同时原子
|
||||||
|
完成两者,也不能在没有回读时声称已写入保留标记。
|
||||||
|
3. **管理凭据来源与 Ready(已确认)**:Instance 引用 controller namespace 内的
|
||||||
|
管理 Secret 名称和字段;管理员维护 ExternalSecret,ESO 同步。controller 不直接
|
||||||
|
从 Bao 读取管理凭据。已有凭据仍可访问 PG 时,Bao/ESO 故障不撤销 Instance Ready;
|
||||||
|
首次装配无有效 Secret 则失败。Secret 的有效用户名/密码变化时重建管理连接池并
|
||||||
|
重验,不因无关字段变化重建;controller 不修改 PG 密码,不回写 Secret 或 Bao。
|
||||||
|
4. **绑定时机**:系统规格写“首次成功后不可变”,API 文档写“首次创建外部状态后
|
||||||
|
不可变”。应明确绑定在认领、首次外部写入还是 Ready 时固定,及如何在 status 丢失
|
||||||
|
后恢复;否则供应中途改名称可能产生无人管理的资源。
|
||||||
|
|
||||||
|
5. **Instance 删除(已确认)**:开始受管即添加 finalizer;删除期间停止新供应,
|
||||||
|
有 Tenant 引用就等待,无引用才解除,不级联删除外部资源。首版采用 finalizer
|
||||||
|
与引用检查,不引入跨对象锁/准入控制;不保证并发创建与删除的原子性。
|
||||||
|
|
||||||
|
Tenant 的 Retain 等待决行为留到 Tenant 设计,不属于本轮 Instance 设计范围。
|
||||||
|
相关用例在决策批准前不进入实现,不同时实现整套模型。
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
# 现有数据库迁移 Runbook
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 状态 | Review;尚未在临时 PostgreSQL 演练 |
|
||||||
|
| 适用范围 | 任意既有数据库迁移为新建 v1alpha1 Tenant |
|
||||||
|
| 最后更新 | 2026-09-10 |
|
||||||
|
|
||||||
|
v1alpha1 不接管现有 database、role 或 OpenBao record。本流程通过逻辑 dump/restore 把
|
||||||
|
数据迁移到 controller 创建的新资源,保留旧资源作为限时回滚点。
|
||||||
|
|
||||||
|
以下命令是顺序模板,不可原样复制到真实环境。先把尖括号变量解析成明确值,确认当前
|
||||||
|
连接目标,再逐条执行。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。确认:
|
||||||
|
|
||||||
|
- registry 记录 UID 正确;
|
||||||
|
- OpenBao metadata 属于该 Tenant;
|
||||||
|
- 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,禁止
|
||||||
|
通过 Tenant `Delete` 清理。安全删除 dump 和临时凭据材料,并记录验证结果。
|
||||||
|
|
||||||
|
## 回滚
|
||||||
|
|
||||||
|
在新目标出现问题且旧资源仍保留时:
|
||||||
|
|
||||||
|
1. 立即停止新目标写入。
|
||||||
|
2. 评估切换后是否产生新数据;若有,先决定反向迁移或接受丢弃,不能盲目切回。
|
||||||
|
3. 将应用连接切回 retained database/role;若必须恢复原名称,先确保新受管目标已用
|
||||||
|
`Delete` 完整清理或改用不同名称,再安全地反向执行 rename。
|
||||||
|
4. 恢复旧凭据(MD5 环境可能需要重设),验证旧服务。
|
||||||
|
5. 保留失败 Tenant 供排障;选择 Retain 或 Delete 前明确其外部资源后果。
|
||||||
|
|
||||||
|
若已经删除旧资源,则只能使用已验证备份恢复,不再属于本 runbook 的快速回滚。
|
||||||
|
|
||||||
|
## 演练验收
|
||||||
|
|
||||||
|
发布首个可用版本前,必须在临时 PostgreSQL/OpenBao/Kind 环境执行本文并记录:
|
||||||
|
|
||||||
|
- 使用的 PostgreSQL major version 和命令版本;
|
||||||
|
- dump/restore 返回码和对象差异;
|
||||||
|
- DNS/IP TLS 登录结果;
|
||||||
|
- ESO 投射与应用启动结果;
|
||||||
|
- 回滚演练结果;
|
||||||
|
- 哪些命令或前置检查需要修订。
|
||||||
|
|
||||||
|
完成演练前,本文不得标记为 `Verified`。
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# 运维与故障处理
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 状态 | Review;命令待实现后演练 |
|
||||||
|
| 最后更新 | 2026-09-10 |
|
||||||
|
|
||||||
|
## 日常检查
|
||||||
|
|
||||||
|
先看 API 合同,而不是从日志猜状态:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
kubectl get postgresqlinstances
|
||||||
|
kubectl get postgresqltenants -A
|
||||||
|
kubectl get postgresqltenant -n <namespace> <name> -o yaml
|
||||||
|
kubectl describe postgresqltenant -n <namespace> <name>
|
||||||
|
```
|
||||||
|
|
||||||
|
随后检查 controller 日志、ExternalSecret/Secret、OpenBao metadata、registry 和
|
||||||
|
PostgreSQL catalog。排障时不得把 Secret data 或带 Token 的请求粘贴到 issue/日志。
|
||||||
|
`status.phase` 是 controller 状态机 checkpoint,也用于定位当前步骤;`Ready`
|
||||||
|
Condition/Reason 用于判断对外结果。phase 不能替代外部事实,清空或不一致时应由
|
||||||
|
controller 自动重建/纠正。
|
||||||
|
|
||||||
|
## 常见 Reason
|
||||||
|
|
||||||
|
| Reason | 首要检查 |
|
||||||
|
| --- | --- |
|
||||||
|
| `InvalidSpec` / `ImmutableField` | API 字段、identifier、不可变/只追加约束 |
|
||||||
|
| `DependencyUnavailable` | 网络、DNS、服务状态和超时 |
|
||||||
|
| `AuthenticationFailed` | 管理凭据、CA、DNS/IP SAN、OpenBao auth |
|
||||||
|
| `InsufficientPrivileges` | PostgreSQL grants、OpenBao policy、Kubernetes RBAC |
|
||||||
|
| `InstanceNotReady` | 先恢复所引用 Instance |
|
||||||
|
| `Conflict` | registry UID、同名 DB/role、OpenBao metadata;禁止直接覆盖 |
|
||||||
|
| `CredentialProjectionFailed` | ClusterSecretStore、ExternalSecret Condition、目标 Secret |
|
||||||
|
| `ProvisioningFailed` | `status.phase` 及对应外部资源的回读结果 |
|
||||||
|
|
||||||
|
修复依赖后让正常 reconcile 自动重试。不要通过删除/重建 CR 规避 Conflict;新 UID 只会
|
||||||
|
使已有保留资源继续冲突。
|
||||||
|
|
||||||
|
## Retain 后的资源
|
||||||
|
|
||||||
|
Retain 删除完成后,database、role、OpenBao record 和 registry 所有权记录仍存在但标记
|
||||||
|
unmanaged。v1alpha1 不支持重新关联。需要恢复管理时,使用 [`migration.md`](migration.md)
|
||||||
|
把数据迁移到一个全新受管名称;不要手工把 registry UID 改成新 CR UID。
|
||||||
|
|
||||||
|
## Delete 卡住
|
||||||
|
|
||||||
|
1. 暂停应用写入并记录 Tenant UID、Instance UID、database、role 和 Bao path。
|
||||||
|
2. 从 registry 和 OpenBao metadata 独立确认所有权。
|
||||||
|
3. 检查删除阶段,修复 PostgreSQL/OpenBao/ESO 依赖,让 controller 继续。
|
||||||
|
4. 若依赖永久丢失,列出每个可能残留的 database、role、KV metadata 和 Secret。
|
||||||
|
5. 只有确认接受这些残留后,才人工移除 finalizer。
|
||||||
|
|
||||||
|
最终 finalizer 名称由 API 实现固定后补入命令。人工移除 finalizer不会执行剩余清理,
|
||||||
|
也不会把外部资源变成可由新 CR 接管的资源。
|
||||||
|
|
||||||
|
## 备份与恢复
|
||||||
|
|
||||||
|
- PostgreSQL VM/磁盘备份必须与数据库一致性策略配套;仅复制在线磁盘不自动等于有效
|
||||||
|
PostgreSQL 备份。
|
||||||
|
- PostgreSQL 备份必须包含管理 database 中的 controller registry。
|
||||||
|
- OpenBao 使用独立的受支持备份/快照流程,且恢复点应与 PostgreSQL 尽量接近。
|
||||||
|
- Kubernetes 侧备份 CR、controller 配置、ClusterSecretStore 和公开 CA bundle,不备份
|
||||||
|
明文 Secret 作为凭据事实来源。
|
||||||
|
- 定期在隔离环境执行恢复演练,验证 registry、KV metadata、应用登录及 Retain/Delete。
|
||||||
|
|
||||||
|
恢复后先停止 controller,核对 PostgreSQL/OpenBao 时间点与 UID 映射,再启动单副本
|
||||||
|
controller 观察;出现一侧存在、一侧缺失时不得手工生成新密码或改 registry,应先按
|
||||||
|
Conflict 处理并决定恢复哪一侧。
|
||||||
|
|
||||||
|
## 升级与紧急停止
|
||||||
|
|
||||||
|
有疑似越权删除或凭据泄漏时,先把 controller Deployment scale 到 0,保留 CR、registry
|
||||||
|
和日志证据,再撤销 OpenBao token/role 并限制 PostgreSQL 管理 role。恢复前在隔离环境
|
||||||
|
复现并确认不会扩大破坏。一般依赖故障无需 scale down,最终一致性会自动重试。
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# 安全模型
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 状态 | Review |
|
||||||
|
| 最后更新 | 2026-09-10 |
|
||||||
|
|
||||||
|
## 保护目标
|
||||||
|
|
||||||
|
- 应用密码只存在于 OpenBao、ESO 投射的目标 Secret 和需要使用它的进程内存中。
|
||||||
|
- controller 只能修改其 registry 能证明归属当前 Tenant UID 的资源。
|
||||||
|
- namespace 租户不能越权管理 Instance、其他 namespace 或 controller 配置。
|
||||||
|
- PostgreSQL 和 OpenBao 的网络身份使用受信 CA 验证,不因 DNS 不可用而降级 TLS。
|
||||||
|
|
||||||
|
## 信任边界
|
||||||
|
|
||||||
|
Kubernetes 管理员、OpenBao 管理员和 PostgreSQL 管理员是平台信任主体。能读取 Tenant
|
||||||
|
目标 Secret 或对应 OpenBao path 的主体等同于持有数据库账号。database owner 可以
|
||||||
|
改变自己 database 内的对象,因此 COMMENT 不能作为 controller 所有权依据。
|
||||||
|
|
||||||
|
VM/磁盘备份会包含 PostgreSQL registry 和租户数据,但不应包含 OpenBao 中的密码;完整
|
||||||
|
灾难恢复必须同时保护 PostgreSQL 与 OpenBao,并控制两份备份的访问权限。
|
||||||
|
|
||||||
|
## 凭据处理
|
||||||
|
|
||||||
|
- controller 使用 Kubernetes auth 获取短期 OpenBao token,不配置长期静态 token。
|
||||||
|
- 管理凭据只从 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。
|
||||||
|
|
||||||
|
PostgreSQL 管理 role 不应是 superuser。若平台选择 SECURITY DEFINER 函数承载创建或
|
||||||
|
删除操作,函数必须固定 `search_path`、严格校验 identifier、拒绝任意 SQL,并仅向
|
||||||
|
controller role 授予 EXECUTE。controller 不调用 shell 或 `psql` 拼接用户输入。
|
||||||
|
|
||||||
|
Kubernetes RBAC 应把 cluster-scoped Instance 管理限制给平台管理员。Tenant editor
|
||||||
|
不自动获得 Secret read;是否读取目标 Secret 由 namespace 内独立 RBAC 决定。
|
||||||
|
|
||||||
|
## 删除保护
|
||||||
|
|
||||||
|
Delete 是明确的数据销毁授权,但仍必须在每一步校验 Instance UID、Tenant UID、名称和
|
||||||
|
OpenBao metadata。禁止对未知对象使用 `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 只销毁 registry 可证明归属当前 Tenant 的资源。
|
||||||
@@ -0,0 +1,558 @@
|
|||||||
|
# PostgreSQL Tenant Operator 系统规格说明书
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 状态 | Approved |
|
||||||
|
| 目标 API | `database.ayatori.ddupan.top/v1alpha1` |
|
||||||
|
| 最后更新 | 2026-09-13 |
|
||||||
|
| 批准日期 | 2026-09-10 |
|
||||||
|
| 规范范围 | 首次注册外部 PostgreSQL 实例并创建一个应用租户 |
|
||||||
|
|
||||||
|
本文档定义系统对用户和外部依赖呈现的行为,是 API、测试和实现共同遵守的合同。
|
||||||
|
实现若需要改变本文合同,必须先修改规格并重新获得批准。
|
||||||
|
|
||||||
|
文中的“必须”“禁止”“应当”“可以”分别对应强制要求、强制限制、推荐行为和可选
|
||||||
|
行为。
|
||||||
|
|
||||||
|
## 1. 背景
|
||||||
|
|
||||||
|
homelab 中的大部分应用共享一个运行在独立 VM 上的 PostgreSQL DBMS。应用需要各自
|
||||||
|
独立的 database、作为 owner 的 login role 和密码,但不需要独立 PostgreSQL 实例。
|
||||||
|
目前这些资源依靠人工 SQL 和人工 Secret 管理,难以重复、审计和检测漂移。
|
||||||
|
|
||||||
|
本系统使用 Kubernetes CRD 作为声明式 API,持续协调外部 PostgreSQL 与 OpenBao:
|
||||||
|
|
||||||
|
```text
|
||||||
|
PostgreSQLInstance / PostgreSQLTenant
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Ayatori Database controller
|
||||||
|
| |
|
||||||
|
v v
|
||||||
|
PostgreSQL catalog OpenBao KV v2
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 目标
|
||||||
|
|
||||||
|
v1alpha1 必须实现以下目标:
|
||||||
|
|
||||||
|
1. 注册一个已经存在的外部 PostgreSQL 实例并报告连接状态。
|
||||||
|
2. 为一个应用租户创建独立 database 和一个同时作为 database owner 的 login role。
|
||||||
|
3. 根据实例实际可安装扩展列表检查并安装租户申请的 PostgreSQL extension。
|
||||||
|
4. 首次生成高强度长期密码,并只把凭据明文写入 OpenBao KV v2。
|
||||||
|
5. 为 Kubernetes 应用创建 ExternalSecret,由 ESO 将凭据投射到同 namespace Secret。
|
||||||
|
6. 同时输出 OpenBao API URL,使 Kubernetes 外的应用可以直接读取凭据。
|
||||||
|
7. 同时输出 PostgreSQL DNS hostname 和 IP address,不假定所有消费者都能使用集群内
|
||||||
|
DNS。
|
||||||
|
8. 持续检测并修正由本系统管理的非破坏性漂移。
|
||||||
|
9. 通过 Kubernetes Condition 报告进度、成功和可操作的失败原因。
|
||||||
|
10. 重复 reconcile、controller 重启及外部依赖暂时失败不得重复创建或破坏资源。
|
||||||
|
11. 删除 Tenant CR 时默认保留外部资源;显式选择 `Delete` 时提供完整清理路径。
|
||||||
|
|
||||||
|
## 3. 非目标
|
||||||
|
|
||||||
|
v1alpha1 不负责:
|
||||||
|
|
||||||
|
- 创建、升级、备份或高可用运行 PostgreSQL DBMS/VM;
|
||||||
|
- 创建或运维 OpenBao;
|
||||||
|
- 直接写入包含凭据明文的 Kubernetes Secret;Secret 必须由 ESO 投射;
|
||||||
|
- 动态凭据、定时或自动密码轮换;
|
||||||
|
- Web UI、独立 REST API 或 Backstage 插件;
|
||||||
|
- 跨实例迁移 database;
|
||||||
|
- schema/table 级别租户、多 login role 或跨租户 grant;
|
||||||
|
- 删除不属于本系统管理的 database、role、extension 或 OpenBao Secret;
|
||||||
|
- 接管不是由本系统创建的外部资源;
|
||||||
|
- 提供生产环境 SLA。
|
||||||
|
|
||||||
|
## 4. 参与者与事实来源
|
||||||
|
|
||||||
|
| 对象 | 事实来源 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 期望状态 | Kubernetes CR `spec` | 用户声明的合同 |
|
||||||
|
| 最近观察结果与当前阶段 | Kubernetes CR `status` | 可以丢失并重建,不是外部事实来源 |
|
||||||
|
| database/role/grant/extension | PostgreSQL catalog | 每轮 reconcile 必须重新读取 |
|
||||||
|
| 受管资源所有权与保留标记 | PostgreSQL controller registry | 与受管 DBMS 一起备份和恢复 |
|
||||||
|
| controller 工作流阶段 | Kubernetes CR `status.phase` | 状态机 checkpoint;可由外部事实保守重建 |
|
||||||
|
| 应用凭据 | OpenBao KV v2 | Kubernetes API 中不得出现明文 |
|
||||||
|
| Kubernetes 凭据投射 | External Secrets Operator | ExternalSecret 由本 controller 管理 |
|
||||||
|
| PostgreSQL 管理凭据 | controller namespace 的 Kubernetes Secret | 管理员维护 ExternalSecret,由 ESO 同步;Instance 只引用 Secret |
|
||||||
|
|
||||||
|
平台管理员管理 `PostgreSQLInstance`、controller 部署配置、OpenBao policy 和
|
||||||
|
PostgreSQL 管理 role。应用或 GitOps 流程在获得 namespace RBAC 后管理
|
||||||
|
`PostgreSQLTenant`。
|
||||||
|
|
||||||
|
## 5. 资源模型
|
||||||
|
|
||||||
|
### 5.1 PostgreSQLInstance
|
||||||
|
|
||||||
|
`PostgreSQLInstance` 是 cluster-scoped 资源,表示一个已经存在、可由 controller
|
||||||
|
管理的 PostgreSQL server。
|
||||||
|
|
||||||
|
它必须声明:
|
||||||
|
|
||||||
|
- PostgreSQL host、port 和管理连接使用的 database;
|
||||||
|
- PostgreSQL host address,供无法解析 DNS 的消费者使用;
|
||||||
|
- TLS mode;
|
||||||
|
- controller namespace 中 PostgreSQL 管理 Secret 的名称和字段名。
|
||||||
|
|
||||||
|
可安装的 extension 集合由应用层从目标 PostgreSQL 查询,不由管理员在 Instance
|
||||||
|
中声明。v1alpha1 不实现 allowlist;该概念保留为后续可选策略。实际可用不代表安装
|
||||||
|
权限及前置条件已满足,安装仍需执行并回读;查询失败不得被解释为扩展不支持。
|
||||||
|
|
||||||
|
实例 Ready 不代表 PostgreSQL 数据有备份或高可用,只表示 controller 当前可以安全
|
||||||
|
建立管理连接、读取 server metadata、访问 controller registry 并使用所需管理能力。
|
||||||
|
|
||||||
|
Instance 身份和 endpoint 以管理员声明为准。修改 endpoint 不验证是否仍是原物理
|
||||||
|
服务器或原 registry,不增加服务器/安装身份绑定检查;但旧配置观察失效,必须按
|
||||||
|
新配置重新检查连接和管理能力。新 CR 按新 Instance 处理,不自动接管旧 UID 的租户
|
||||||
|
资源。controller 不迁移旧服务器上的数据,也不清理旧目标,影响由管理员负责评估。
|
||||||
|
|
||||||
|
Instance 开始受管时即添加 finalizer,成功保存后才参与供应,不等发现 Tenant 后
|
||||||
|
再补加。删除期间停止新供应;仍有引用它的 Tenant(包括正在删除的 Tenant)时保留
|
||||||
|
finalizer,无引用后才移除。不级联删除 Tenant 或任何外部数据库、角色、凭据。
|
||||||
|
引用检查失败不得当作无引用。首版不引入跨对象锁或准入控制:finalizer 不禁止同时
|
||||||
|
创建 Tenant CR,新 Tenant 遇到正在删除或已不存在的 Instance 时不得开始供应。
|
||||||
|
这不保证列表检查、CR 创建和在途外部操作之间的原子性;不是严格的跨对象事务。
|
||||||
|
|
||||||
|
### 5.2 PostgreSQLTenant
|
||||||
|
|
||||||
|
`PostgreSQLTenant` 是 namespaced 资源。v1alpha1 中,一个 Tenant 精确对应:
|
||||||
|
|
||||||
|
- 一个 `PostgreSQLInstance`;
|
||||||
|
- 一个 database;
|
||||||
|
- 一个同时作为 database owner、供应用使用的 `LOGIN` role;
|
||||||
|
- 零个或多个 extension;
|
||||||
|
- 一个 OpenBao KV v2 凭据位置;
|
||||||
|
- 一个同 namespace ExternalSecret 及其目标 Kubernetes Secret。
|
||||||
|
|
||||||
|
Tenant 的 namespace 用于 Kubernetes RBAC 和身份识别,不代表 PostgreSQL schema。
|
||||||
|
同一 Instance 中的 database 和 role 名称全局唯一。
|
||||||
|
|
||||||
|
## 6. 标识与默认值
|
||||||
|
|
||||||
|
以下是 v1alpha1 的标识合同:
|
||||||
|
|
||||||
|
| 字段 | 默认值 | 约束 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Instance port | `5432` | 1–65535 |
|
||||||
|
| Instance host address | 无 | 必须是合法 IPv4 或 IPv6 address |
|
||||||
|
| 管理 database | `postgres` | 合法 PostgreSQL identifier |
|
||||||
|
| TLS mode | `verify-full` | 禁止隐式降级 |
|
||||||
|
| Tenant database | `metadata.name` | 同一 Instance 全局唯一 |
|
||||||
|
| login role | `metadata.name` | 同一 Instance 全局唯一 |
|
||||||
|
| deletion policy | `Retain` | `Retain` 或 `Delete` |
|
||||||
|
|
||||||
|
Tenant 的 `spec.instanceRef` 与 `metadata.name` 长度合计不得超过 241 个字符,确保
|
||||||
|
派生的 ExternalSecret/Secret 默认名称
|
||||||
|
`<instanceRef>-<metadata.name>-postgresql` 不超过 Kubernetes 253 字符限制。
|
||||||
|
|
||||||
|
固定默认值由 CRD defaulting 写入。依赖 `metadata.name` 或 `instanceRef` 的 database、
|
||||||
|
login role、ExternalSecret/Secret 名称属于 controller 语义默认值:省略字段不会被 admission
|
||||||
|
回写,controller 必须始终计算同一个 effective value,并通过 status 的 database、
|
||||||
|
loginRole、credential reference 以及实际资源展示。
|
||||||
|
v1alpha1 不为此引入 mutating webhook。
|
||||||
|
|
||||||
|
database 和 role 名称必须作为 PostgreSQL identifier 参数安全引用,禁止通过字符串
|
||||||
|
拼接执行。名称校验必须拒绝空字符串、NUL 和超过 PostgreSQL identifier 长度限制的
|
||||||
|
值,并统一限制为小写字母、数字和下划线。
|
||||||
|
|
||||||
|
Tenant 首次成功后,`instanceRef`、database、login role 和凭据位置必须
|
||||||
|
不可变。修改这些字段不是 rename 或 migration,API 必须拒绝或报告明确的
|
||||||
|
`ImmutableField`。
|
||||||
|
|
||||||
|
## 7. PostgreSQL 权限合同
|
||||||
|
|
||||||
|
建议的 v1alpha1 权限模型如下:
|
||||||
|
|
||||||
|
1. database 必须由 login role 拥有。
|
||||||
|
2. login role 必须是 `LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION`。
|
||||||
|
3. 必须撤销 `PUBLIC` 对租户 database 的连接权限,再显式允许 login role 连接。
|
||||||
|
4. controller 不得修改其他 database 或无关 role 的权限。
|
||||||
|
5. controller 只保证请求的 extension 存在;移除 extension 不得自动执行
|
||||||
|
`DROP EXTENSION`。
|
||||||
|
|
||||||
|
这意味着应用可以在自己的 database 内执行 schema migration,但不能创建其他
|
||||||
|
database、role 或访问其他租户。v1alpha1 不创建只有形式意义、却未隔离运行时权限的
|
||||||
|
额外 `NOLOGIN` owner。若以后应用能分别使用 migration 和 runtime 凭据,再通过新的
|
||||||
|
权限 profile 引入 owner/migrator/runtime 角色模型。
|
||||||
|
|
||||||
|
## 8. OpenBao 凭据合同
|
||||||
|
|
||||||
|
### 8.1 Controller 自身认证
|
||||||
|
|
||||||
|
controller 必须使用 Kubernetes auth 登录 OpenBao。controller 使用的 OpenBao API
|
||||||
|
address、提供给消费者的 OpenBao API address、auth mount、auth role 和 KV v2 mount
|
||||||
|
属于部署配置,不属于任何 CR。两个 API address 可以相同;若 controller 使用集群内
|
||||||
|
地址而外部消费者不能解析,则必须单独配置 consumer address。生产部署的 KV mount
|
||||||
|
默认为 `kv`;开发环境可以配置为 OpenBao dev server 默认的 `secret`。长期 OpenBao
|
||||||
|
Token 禁止写入 Deployment、CR 或镜像。
|
||||||
|
|
||||||
|
### 8.2 管理凭据
|
||||||
|
|
||||||
|
`PostgreSQLInstance` 只引用 controller 自身 namespace 中 Kubernetes Secret 的名称及
|
||||||
|
用户名、密码字段名,不允许指定 namespace 或 Bao path。endpoint 仍由 Instance 声明。
|
||||||
|
平台管理员维护 ExternalSecret,将 OpenBao 管理凭据同步到该 Secret;controller 只读
|
||||||
|
Secret,不创建或修改管理 Secret、其 ExternalSecret 或上游管理凭据。
|
||||||
|
|
||||||
|
Instance 管理连接不直接访问 Bao,不负责管理密码轮换。已装配的凭据仍可访问
|
||||||
|
PostgreSQL 且满足 registry/权限要求时,Bao 或 ESO 暂时不可用不使 Instance NotReady。
|
||||||
|
首次装配无法取得有效 Secret 时不能 Ready。检测到所引用 Secret 的有效用户名或密码
|
||||||
|
变化时,controller 使用新值重建管理连接池并重新检查管理能力;仅 metadata 或无关
|
||||||
|
字段变化不触发重建。刷新不依赖 Instance generation 变化,新连接验证失败按实际
|
||||||
|
故障报告,不能用旧连接的成功结果证明新凭据可用。
|
||||||
|
controller 不修改 PostgreSQL 密码、不回写 Secret,也不修改 Bao 管理凭据;数据库侧
|
||||||
|
凭据变更由管理员负责。这是跟随已提供凭据的连接刷新,不是自动密码轮换。
|
||||||
|
Tenant 凭据管理仍直接依赖 Bao。
|
||||||
|
|
||||||
|
### 8.3 租户凭据
|
||||||
|
|
||||||
|
Tenant 不声明凭据 path。controller 根据部署级 KV mount、base path 和 Tenant 的
|
||||||
|
namespace/name 推导唯一的 mount-relative path。base path 来自 controller 启动参数
|
||||||
|
`--openbao-tenant-base-path`,默认 `postgresql-tenants`。最终路径为
|
||||||
|
`<base-path>/<namespace>/<metadata.name>`。推导结果禁止以 `/` 开头,禁止包含空路径段、
|
||||||
|
`.`、`..`,也禁止把 KV v2 HTTP API 的 `data` 或 `metadata` 层编码进路径。
|
||||||
|
|
||||||
|
新 Tenant 的凭据建立顺序必须可从任意中断点恢复:
|
||||||
|
|
||||||
|
1. 验证 Instance、名称、extension 和目标 OpenBao 路径;
|
||||||
|
2. 确认目标 database、role 和 OpenBao 记录不存在,或能够验证为同一 Tenant
|
||||||
|
已创建的部分状态;
|
||||||
|
3. 生成密码;
|
||||||
|
4. 先创建带 controller 所有权 metadata 的 OpenBao KV v2 记录;
|
||||||
|
5. 从 OpenBao 重新读取凭据;
|
||||||
|
6. 使用该凭据创建作为 database owner 的 login role 和其他 PostgreSQL 资源;
|
||||||
|
7. 用 login role 实际连接目标 database;
|
||||||
|
8. 全部验证成功后将 Tenant 标记 Ready。
|
||||||
|
|
||||||
|
若第 4 步成功、后续 PostgreSQL 操作失败,下一轮必须读取同一份 OpenBao 凭据继续,
|
||||||
|
不得生成第二个密码。若 PostgreSQL 先存在而 OpenBao 记录不存在,controller 必须报告
|
||||||
|
Conflict,不得擅自重置已有 role 密码。
|
||||||
|
|
||||||
|
controller 必须在 PostgreSQL 管理 database 的专用 registry schema 中持久保存可验证的
|
||||||
|
Instance UID、Tenant UID 与 namespace/name 关联,不能只依赖会丢失的 CR status 判断
|
||||||
|
资源所有权。registry 必须可回读且不得改变数据库授权语义;database 或 role COMMENT
|
||||||
|
不能作为权威所有权记录。
|
||||||
|
|
||||||
|
默认写入字段固定为:
|
||||||
|
|
||||||
|
```text
|
||||||
|
username
|
||||||
|
password
|
||||||
|
database
|
||||||
|
host
|
||||||
|
hostaddr
|
||||||
|
port
|
||||||
|
sslmode
|
||||||
|
```
|
||||||
|
|
||||||
|
这些字段是 controller 的规范化输出合同。controller 不生成包含密码的 URI、JDBC URL
|
||||||
|
或应用专用键名。应用通过 ExternalSecret template、Helm values 或自身配置把原子字段
|
||||||
|
映射为 `DATABASE_URL`、独立环境变量或配置文件;因此 URI escaping 和应用特有格式也
|
||||||
|
由消费方负责。`host` 是 DNS 名称,`hostaddr` 是可直接连接的 IP;消费者自行选择其
|
||||||
|
支持且可达的连接目标。PostgreSQL server 证书必须同时包含与 `host` 匹配的 DNS SAN
|
||||||
|
和与 `hostaddr` 匹配的 IP SAN,使两种目标都能在 `verify-full` 下独立完成身份验证。
|
||||||
|
|
||||||
|
### 8.4 凭据输出与 ExternalSecret
|
||||||
|
|
||||||
|
controller 必须根据部署级 base path 推导 Tenant 的 KV path,Tenant 不能选择 mount 或
|
||||||
|
任意远端路径。ExternalSecret 固定命名为
|
||||||
|
`<instanceRef>-<metadata.name>-postgresql`。Tenant 可以通过
|
||||||
|
`spec.credential.secretName` 指定目标 Kubernetes Secret 名称;省略时使用同一默认名。
|
||||||
|
自定义名称只需是合法 Kubernetes Secret 名称,不限制命名内容;两者均与 Tenant 位于
|
||||||
|
同一 namespace。
|
||||||
|
|
||||||
|
controller 必须创建同 namespace ExternalSecret,从固定的 ClusterSecretStore 读取七个
|
||||||
|
原子字段。ExternalSecret 及目标 Secret 的名称通过 Tenant status 暴露。controller
|
||||||
|
不得直接读取 OpenBao 密码后写入 Kubernetes Secret。
|
||||||
|
|
||||||
|
Tenant status 还必须提供完整、可由外部消费者使用的 OpenBao KV v2 API URL。URL 可以
|
||||||
|
包含 consumer API address、mount 和 secret path,但不得包含 Token、密码或其他认证
|
||||||
|
信息。默认 `kubectl get` 表格显示目标 Secret 名称;完整 OpenBao URL 通过
|
||||||
|
`kubectl get postgresqltenant <name> -o yaml` 获取,避免表格列过长。
|
||||||
|
|
||||||
|
OpenBao metadata 必须能够标识 Tenant UID、namespace/name 和 Instance,使 controller
|
||||||
|
区分自己的残留记录与外部记录。任何凭据值都不得进入日志、Event、Condition、metric
|
||||||
|
label、trace、CR spec/status 或测试快照。
|
||||||
|
|
||||||
|
## 9. Reconcile 行为
|
||||||
|
|
||||||
|
系统采用最终一致性模型。Kubernetes、PostgreSQL、OpenBao 和 ESO 可以短暂处于不同
|
||||||
|
阶段;controller 不尝试实现跨系统事务,而是以 Kubernetes CR `status.phase` 作为
|
||||||
|
工作流 checkpoint,通过幂等外部操作和每轮回读验证最终收敛。
|
||||||
|
|
||||||
|
两个 CR 的状态机权威记录都在 `status.phase`。controller 根据 phase 选择下一项候选
|
||||||
|
动作,但 phase 不能替代外部状态检查:执行前后仍须回读 PostgreSQL catalog、registry、
|
||||||
|
OpenBao 和 Kubernetes/ESO。外部写入成功但 status 更新失败时,下一轮必须识别已完成
|
||||||
|
事实并推进 phase,不得重复生成密码或报告虚假冲突。
|
||||||
|
|
||||||
|
status 丢失时,controller 必须从 registry 的所有权记录和各外部系统实际状态保守重建
|
||||||
|
phase。若 status 被伪造或领先于实际状态,controller 必须纠正到安全阶段并补齐资源,
|
||||||
|
不能跳过验证。registry 不保存或驱动协调 phase。
|
||||||
|
|
||||||
|
Instance phase 按当前 generation 表示连接与初始化进度:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Pending -> Validating -> InitializingRegistry -> Ready
|
||||||
|
(any phase) --------------------------------> Deleting
|
||||||
|
```
|
||||||
|
|
||||||
|
spec generation 改变后可以从 `Ready` 回到 `Validating`。Tenant phase 如下:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Pending -> Planned -> CredentialCreated -> RoleCreated -> DatabaseCreated
|
||||||
|
-> ExternalSecretCreated -> CredentialProjected -> Ready -> Deleting
|
||||||
|
```
|
||||||
|
|
||||||
|
失败不增加 `Failed` phase;phase 保留在无法推进的步骤,由 `Ready=False` 的 Reason 和
|
||||||
|
message 表达 `Conflict`、认证失败或依赖不可用。Retain 删除完成后 CR 已不存在,因此
|
||||||
|
没有持久的 `Retained` phase。
|
||||||
|
|
||||||
|
每轮 Tenant reconcile 必须按以下逻辑执行:
|
||||||
|
|
||||||
|
```text
|
||||||
|
读取 Tenant
|
||||||
|
-> 读取 Instance
|
||||||
|
-> 校验不可变字段与输入
|
||||||
|
-> 检查 Instance Ready
|
||||||
|
-> 读取 OpenBao 与 PostgreSQL 实际状态
|
||||||
|
-> 检测冲突或部分完成状态
|
||||||
|
-> 执行非破坏性补齐
|
||||||
|
-> 使用应用凭据验证登录
|
||||||
|
-> 创建并验证 ExternalSecret/Secret 投射
|
||||||
|
-> 回读实际状态
|
||||||
|
-> 更新 status
|
||||||
|
```
|
||||||
|
|
||||||
|
要求:
|
||||||
|
|
||||||
|
- 所有步骤必须幂等;
|
||||||
|
- 每个外部写入前必须先在 CR status 持久化足够的操作意图,写入后必须回读并推进
|
||||||
|
`status.phase`;
|
||||||
|
- 暂时性网络、锁和依赖错误必须重试;
|
||||||
|
- 输入错误、资源冲突和禁止操作不得忙循环重试,只在 generation 或依赖状态变化后
|
||||||
|
重试;
|
||||||
|
- 未知外部资源不得被修改、接管或删除;
|
||||||
|
- 用户从 `spec.extensions` 移除 extension 时不得执行卸载,必须报告该字段在 v1alpha1
|
||||||
|
中只允许追加;
|
||||||
|
- controller 重启不得影响已经签发的应用密码;
|
||||||
|
- `status` 丢失后必须可以从 registry、PostgreSQL、OpenBao 和 Kubernetes/ESO 重建。
|
||||||
|
|
||||||
|
## 10. Condition 合同
|
||||||
|
|
||||||
|
两个资源都必须提供唯一的 `Ready` Condition。可以增加辅助 Condition,但调用方只需
|
||||||
|
依赖 `Ready`。
|
||||||
|
|
||||||
|
| 状态 | 含义 |
|
||||||
|
| --- | --- |
|
||||||
|
| `Ready=Unknown` | 正在首次观察或 reconcile,尚无结论 |
|
||||||
|
| `Ready=False` | 当前 generation 未达到合同要求 |
|
||||||
|
| `Ready=True` | 当前 generation 已回读验证成功 |
|
||||||
|
|
||||||
|
Condition 必须带正确的 `observedGeneration`。资源自身的
|
||||||
|
`status.observedGeneration` 只在当前 generation 完成一次有结论的 reconcile 后更新。
|
||||||
|
|
||||||
|
最低 Reason 集合:
|
||||||
|
|
||||||
|
| Reason | 适用资源 | 含义 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Reconciling` | 两者 | 尚在处理 |
|
||||||
|
| `Ready` | 两者 | 当前 generation 已验证 |
|
||||||
|
| `InvalidSpec` | 两者 | 输入不满足规格 |
|
||||||
|
| `DependencyUnavailable` | 两者 | PostgreSQL 或 OpenBao 暂时不可用 |
|
||||||
|
| `AuthenticationFailed` | Instance | 管理凭据或 TLS 验证失败 |
|
||||||
|
| `InsufficientPrivileges` | Instance | 管理 role 缺少必要权限 |
|
||||||
|
| `InstanceNotReady` | Tenant | 引用的 Instance 未 Ready |
|
||||||
|
| `Conflict` | Tenant | 目标名称或 OpenBao 路径已被其他主体占用 |
|
||||||
|
| `ProvisioningFailed` | Tenant | 可重试的创建/验证失败 |
|
||||||
|
| `CredentialProjectionFailed` | Tenant | ESO 或目标 Secret 未达到期望状态 |
|
||||||
|
|
||||||
|
Condition message 必须适合人类排障,但禁止包含连接串密码、Token 或完整 Secret 数据。
|
||||||
|
|
||||||
|
## 11. 删除与保留
|
||||||
|
|
||||||
|
### 11.1 Retain
|
||||||
|
|
||||||
|
`Retain` 是默认策略:
|
||||||
|
|
||||||
|
- 删除 Tenant CR 不得删除 database、role、extension 或 OpenBao 记录;
|
||||||
|
- controller 不得因外部依赖不可用而永久阻止 Retain CR 删除;
|
||||||
|
- 保留资源必须继续携带原 Tenant UID 和 namespace/name 的所有权记录,但在 CR 删除后
|
||||||
|
明确处于 unmanaged 状态;
|
||||||
|
- 重新创建同名 Tenant 会产生新的 UID,必须因已有资源不属于新 UID 而报告 Conflict;
|
||||||
|
- v1alpha1 不提供重新关联、import 或 adoption;恢复管理必须使用第 12 节的迁移流程,
|
||||||
|
或等待后续版本定义显式纳管协议。
|
||||||
|
|
||||||
|
### 11.2 Delete
|
||||||
|
|
||||||
|
用户在创建 Tenant 时显式设置 `deletionPolicy: Delete`,表示删除 CR 时授权永久清理
|
||||||
|
该 Tenant 的外部资源。controller 必须使用 finalizer,并按以下顺序处理:
|
||||||
|
|
||||||
|
1. 再次验证 database、role 和 OpenBao 记录都属于当前 Tenant UID;
|
||||||
|
2. 删除 ExternalSecret,并确认目标 Kubernetes Secret 已删除;
|
||||||
|
3. 禁止该 login role 建立新连接;
|
||||||
|
4. 终止该 database 的现有连接;
|
||||||
|
5. 删除 database,database 内 extension 随之删除;
|
||||||
|
6. 删除 login role;
|
||||||
|
7. 删除 OpenBao KV 记录及其可恢复版本;
|
||||||
|
8. 回读确认外部资源均不存在;
|
||||||
|
9. 删除 controller registry 记录;
|
||||||
|
10. 移除 finalizer,允许 Kubernetes 删除 CR。
|
||||||
|
|
||||||
|
任一步失败都必须保持 finalizer 并从安全检查开始重试。controller 禁止使用
|
||||||
|
`CASCADE` 删除无法证明属于该 Tenant 的依赖对象。若 Instance 或 OpenBao 永久丢失,
|
||||||
|
管理员可以在核实外部状态后手工移除 finalizer;该逃生操作必须在运维 runbook 中明确
|
||||||
|
标记为可能遗留资源。
|
||||||
|
|
||||||
|
v1alpha1 不自动检查备份,也不承诺恢复被 `Delete` 删除的数据。显式选择 Delete 的
|
||||||
|
用户承担数据销毁语义;默认 Retain 用于避免普通误删。
|
||||||
|
|
||||||
|
## 12. 现有环境迁移
|
||||||
|
|
||||||
|
v1alpha1 不接管现有 database 或 role,但必须提供可重复、可回滚的迁移 runbook。对每
|
||||||
|
个现有应用租户,推荐的停机迁移顺序是:
|
||||||
|
|
||||||
|
1. 盘点 database、role、owner、grant 和 extension,并完成可恢复备份;
|
||||||
|
2. 创建逻辑备份,必须使用可映射到新 owner 的格式,避免恢复旧 role ownership;
|
||||||
|
3. 停止应用写入并确认没有活动写事务;
|
||||||
|
4. 完成最终逻辑备份;
|
||||||
|
5. 将旧 database 和 role 重命名为带迁移时间戳的保留名称,释放最终名称;
|
||||||
|
6. 创建 `PostgreSQLTenant`,由 controller 创建最终 database、role 和 OpenBao 凭据;
|
||||||
|
7. 等待 Tenant Ready;
|
||||||
|
8. 以新 owner 恢复逻辑备份,并验证 row count、schema、extension 和应用权限;
|
||||||
|
9. 让 ESO 投射新凭据,重启或重新部署应用;
|
||||||
|
10. 验证应用读写后结束维护窗口;
|
||||||
|
11. 保留旧 database、role 和备份直到回滚窗口结束,再由管理员手工清理。
|
||||||
|
|
||||||
|
回滚时停止新应用写入、恢复原名称或连接配置,并重新使用旧凭据。迁移工具不得把旧
|
||||||
|
密码、管理凭据或 dump 文件提交到 Git。真实命令、锁定方式和各现有应用验证项见
|
||||||
|
[`migration.md`](migration.md),并必须在实现首个可用版本前通过临时 PostgreSQL 实例
|
||||||
|
演练。
|
||||||
|
|
||||||
|
## 13. 安全要求
|
||||||
|
|
||||||
|
1. 所有 PostgreSQL 与 OpenBao 网络访问必须支持超时和 context cancellation。
|
||||||
|
2. homelab 部署必须通过 Deployment 挂载的共享 CA bundle 验证 TLS server identity;
|
||||||
|
该 bundle 的信任根来自 OpenBao PKI,但不得包含 CA 私钥。Instance 默认使用
|
||||||
|
`verify-full`,其 host 必须与服务器证书名称匹配。开发环境可以显式使用 `disable`
|
||||||
|
明文连接。
|
||||||
|
3. PostgreSQL 管理 role 应使用满足本规格的最小权限,不应使用 PostgreSQL
|
||||||
|
superuser;若 extension 安装需要额外权限,必须单独记录例外。
|
||||||
|
4. controller 的 OpenBao policy 仅覆盖受管租户 KV 操作,不授予管理凭据路径权限。
|
||||||
|
管理凭据的 ESO 同步身份与应用凭据的 ESO 读取身份隔离。controller 只在自身
|
||||||
|
namespace 获得管理 Secret 读取权限,不因此扩大跨 namespace Secret data 访问范围。
|
||||||
|
5. namespace 用户不得修改 cluster-scoped Instance。
|
||||||
|
6. 所有 identifier、extension name 和引用字段必须在发起外部调用前校验。
|
||||||
|
7. controller 不得通过 shell 或 `psql` 子进程执行用户输入。
|
||||||
|
8. 错误包装、结构化日志和 tracing 必须经过 Secret 泄露测试。
|
||||||
|
|
||||||
|
详细威胁模型和部署 policy 见 [`security.md`](security.md)。
|
||||||
|
|
||||||
|
## 14. 可观测性要求
|
||||||
|
|
||||||
|
v1alpha1 至少必须提供:
|
||||||
|
|
||||||
|
- Kubernetes Events:开始 provisioning、成功及需要人工处理的失败;
|
||||||
|
- 结构化日志:resource namespace/name、Instance、generation、阶段和错误类别;
|
||||||
|
- controller-runtime 默认 reconcile metrics;
|
||||||
|
- 不包含 database、role、OpenBao path 等无界用户输入的低基数失败分类 metric。
|
||||||
|
|
||||||
|
日志和 metrics 的存在不能代替 Condition;Condition 是 API 使用者判断状态的主要方式。
|
||||||
|
|
||||||
|
## 15. 验收标准
|
||||||
|
|
||||||
|
实现 v1alpha1 第一条完整纵向切片前,测试必须覆盖:
|
||||||
|
|
||||||
|
1. 有效 Instance 可以建立 TLS 管理连接并变为 Ready。
|
||||||
|
2. PostgreSQL 管理能力不可用时 Instance Ready=False,恢复后自动变为 Ready;已有
|
||||||
|
管理凭据可正常使用时,Bao/ESO 故障不单独影响 Instance Ready。首次装配缺少有效
|
||||||
|
管理 Secret 时不能 Ready;Tenant 的 Bao 操作失败按其自身依赖故障报告。
|
||||||
|
3. 有效 Tenant 创建 database、作为 owner 的 login、grant、extension 和 OpenBao
|
||||||
|
记录。
|
||||||
|
4. 应用凭据可以实际连接且不能创建其他 database/role。
|
||||||
|
5. 相同 generation 重复 reconcile 不改变密码、不重复创建资源。
|
||||||
|
6. controller 在每个外部写入步骤后中断,重启后都能继续并得到相同最终状态。
|
||||||
|
7. 预先存在且不属于当前 Tenant UID 的 database、role 或 OpenBao path 导致
|
||||||
|
Conflict,且不修改已有资源。
|
||||||
|
8. 目标实例实际不支持的 extension 在供应外部写入前被拒绝;扩展列表查询失败时
|
||||||
|
按依赖故障处理,不报告为不支持。安装结果仍须回读验证。
|
||||||
|
9. status 被清空后可以从两个外部事实来源重建。
|
||||||
|
10. 删除 Retain Tenant 后外部资源仍存在且不再受管;重新创建同名 Tenant 报告
|
||||||
|
Conflict。
|
||||||
|
11. 日志、Event、Condition、metric 和 CR 中不存在生成的密码或管理凭据。
|
||||||
|
12. 两个 namespace 对同一 Instance 申请相同名称时,只有第一个成功,第二个报告
|
||||||
|
Conflict。
|
||||||
|
13. 删除 Delete Tenant 时,任一步骤失败都可重试,且最终删除 database、login role、
|
||||||
|
OpenBao KV 历史和 finalizer。
|
||||||
|
14. 使用迁移 runbook 可以把一个现有 database 转移到新建的受管 database,并在回滚
|
||||||
|
窗口内恢复旧服务。
|
||||||
|
15. Tenant 只有在 ExternalSecret Ready、目标 Secret 存在且应用凭据实际可登录后才
|
||||||
|
Ready。
|
||||||
|
16. Tenant status 同时提供 Kubernetes Secret reference 和不含认证信息的 OpenBao API
|
||||||
|
URL。
|
||||||
|
17. DNS 不可用时,使用输出的 `hostaddr` 可以连接 PostgreSQL;server 证书同时覆盖
|
||||||
|
`host` 的 DNS SAN 和 `hostaddr` 的 IP SAN,两种连接目标均可通过 `verify-full`。
|
||||||
|
18. 两个 CR 的 `status.phase` 都能反映当前协调步骤;清空 status 后可以从外部事实重建,
|
||||||
|
且伪造或过期 phase 不会使 controller 跳过验证或外部操作。
|
||||||
|
|
||||||
|
单元测试验证纯决策逻辑,adapter 集成测试使用 Docker PostgreSQL/OpenBao,controller
|
||||||
|
集成测试使用 envtest,完整网络路径使用 Kind E2E。
|
||||||
|
|
||||||
|
## 16. 已确认决策
|
||||||
|
|
||||||
|
- v1alpha1 使用一个同时作为 database owner 的 login role,不创建额外 NOLOGIN owner。
|
||||||
|
- v1alpha1 不接管任意现有资源,但必须提供并演练 dump/restore 迁移路径。
|
||||||
|
- v1alpha1 同时实现默认 `Retain` 和显式 `Delete`;Delete 必须有 finalizer、所有权验证
|
||||||
|
和完整清理路径。
|
||||||
|
- OpenBao KV v2 mount 和 base path 是 controller 部署配置,mount 默认 `kv`,base path
|
||||||
|
由 `--openbao-tenant-base-path` 配置并默认 `postgresql-tenants`;Tenant 不能选择 mount
|
||||||
|
或任意远端 path,controller 根据 namespace/name 推导记录路径。
|
||||||
|
- 租户 KV 记录固定写入 `username/password/database/host/hostaddr/port/sslmode` 七个
|
||||||
|
原子字段;
|
||||||
|
controller 不生成连接 URI,应用负责映射和拼装自身配置。
|
||||||
|
- PostgreSQL TLS 使用 controller Deployment 挂载的共享 CA bundle。OpenBao PKI 是
|
||||||
|
CA 权威并继续签发、续期 PostgreSQL server 证书;controller 只消费公开 trust
|
||||||
|
bundle,不接触 CA 私钥。bundle 可以由 ConfigMap 或现有证书同步机制投射,不允许
|
||||||
|
Tenant 或 Instance 选择其他 CA;开发环境可以显式使用 `sslMode: disable`。
|
||||||
|
- 每个 PostgreSQLInstance 在其管理 database 中维护 controller 专用 registry schema。
|
||||||
|
registry 是受管资源所有权、安装身份和 Retain 后 unmanaged 标记的权威记录;两个
|
||||||
|
CR 的 `status.phase` 是 controller 状态机的权威 checkpoint,Instance status 不聚合
|
||||||
|
Tenant 清单。
|
||||||
|
- PostgreSQL database 和 role identifier 必须匹配 `^[a-z][a-z0-9_]{0,62}$`,不支持
|
||||||
|
需要双引号的大小写或特殊字符名称。
|
||||||
|
- External Secrets Operator 是 v1alpha1 的运行依赖。controller 管理同 namespace
|
||||||
|
ExternalSecret,但不直接写明文 Secret;Tenant status 同时输出目标 Secret reference
|
||||||
|
和供非 Kubernetes 消费者使用的 OpenBao API URL。
|
||||||
|
- PostgreSQLInstance 同时声明 DNS `host` 和 IP `hostaddr`;PostgreSQL server 证书必须
|
||||||
|
同时包含对应 DNS SAN 和 IP SAN,消费者自行选择连接目标。
|
||||||
|
|
||||||
|
## 17. 批准状态
|
||||||
|
|
||||||
|
2026-09-14 确认 extension 判定修订:v1alpha1 使用实例实际可安装列表,不实现管理员
|
||||||
|
allowlist;后续可按需引入策略。现有 allowedExtensions 字段尚待 API 实现移除。
|
||||||
|
|
||||||
|
2026-09-13 已确认管理连接修订:Instance 引用 controller namespace 内的管理 Secret,
|
||||||
|
管理员维护 ExternalSecret,由 ESO 同步;controller 不再从 Bao 直接读取管理凭据。
|
||||||
|
此项是已批准行为,现有 API types 与实现尚待后续修改。
|
||||||
|
|
||||||
|
具体设计决策和本文整体已于 2026-09-10 获得批准,可以进入 API reference、测试和
|
||||||
|
实现阶段。同日确认状态机修订:两个 CR 的 `status.phase` 是 controller 工作流的权威
|
||||||
|
checkpoint;PostgreSQL registry 只承担所有权、安装身份和保留状态。
|
||||||
|
|
||||||
|
## 18. 与当前脚手架的已知差异
|
||||||
|
|
||||||
|
当前 API skeleton 至少需要以下调整:
|
||||||
|
|
||||||
|
- 删除 Tenant 自选 OpenBao path 的能力,改由部署级 mount、base path 和 Tenant
|
||||||
|
identity 推导,并修正当前包含 `kv/` 前缀的示例;
|
||||||
|
- 增加 controller 部署级 OpenBao KV mount 和 TLS 配置;
|
||||||
|
- 增加部署级 OpenBao consumer address、ClusterSecretStore 和 KV base path 配置;
|
||||||
|
- 删除独立 `ownerRole` 字段,使 login role 成为 database owner;
|
||||||
|
- 为 Instance 增加 `hostaddr`,为 Tenant 增加目标 Secret 配置及 Secret/Bao URL 输出
|
||||||
|
status;
|
||||||
|
- 按已确认的 identifier 合同收紧校验;
|
||||||
|
- 增加 PostgreSQL controller registry,记录基于 UID 的所有权、安装身份和保留状态;
|
||||||
|
- 修正凭据 type 中遗留的 rotation 注释;
|
||||||
|
- 使 Condition、不可变字段和 extension 追加语义具备 API 校验或明确的 reconcile
|
||||||
|
结果。
|
||||||
|
|
||||||
|
这些是规格批准后的实现工作,不属于本规格本身。
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# ADR-0001:采用 Kubernetes API 作为资源模型
|
# ADR-0001:采用 Kubernetes API machinery 作为状态协调平面
|
||||||
|
|
||||||
- 状态:Accepted
|
- 状态:Accepted
|
||||||
- 日期:2026-09-17
|
- 日期:2026-09-17
|
||||||
@@ -10,16 +10,49 @@ homelab 的基础设施状态分散在多套工具和后端中。仅集中 IaC
|
|||||||
|
|
||||||
## 决策
|
## 决策
|
||||||
|
|
||||||
Ayatori 使用 Kubernetes API machinery 与 CRD 表达平台资源、引用和状态,但不将平台
|
Ayatori 使用 kube-apiserver、etcd、Kubernetes API machinery 与 CRD 构成 API 和状态协调
|
||||||
限定为容器编排系统。Controller 可以运行于专用 management environment,并管理集群外
|
平面。主要复用的是以下难以可靠重建的能力:
|
||||||
的 VM、LB、数据库、对象存储、DNS、凭据和托管 Kubernetes 控制面。
|
|
||||||
|
- 版本化对象 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 是当前意图、关系和状态的在线控制面;真实后端
|
GitOps 是长期期望状态的主要提交入口;API 是当前意图、关系和状态的在线控制面;真实后端
|
||||||
仍是运行事实来源。Controller 负责三者之间持续收敛。
|
仍是运行事实来源。Controller 负责三者之间持续收敛。
|
||||||
|
|
||||||
|
`generic-apiserver` 或 Kubernetes API aggregation 只会让 Ayatori 接管资源的服务端实现,并不会
|
||||||
|
替代上述领域 controller。除非 CRD/kube-apiserver 的存储模型、API 语义或扩展边界形成经过验证的
|
||||||
|
阻碍,Ayatori 不自行承担 watch、RBAC、API 兼容、存储版本迁移和高可用 API Server 的实现与运维。
|
||||||
|
|
||||||
## 结果
|
## 结果
|
||||||
|
|
||||||
- 获得统一声明式 API、watch、RBAC、admission、conditions 和 controller 生态。
|
- 获得统一声明式 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、认证、备份和控制面升级。
|
- 必须维护 CRD 版本、conversion、认证、备份和控制面升级。
|
||||||
- 不在 API 中保存日志、指标、大对象或业务数据,只保存控制所需状态及引用。
|
- 不在 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,92 @@
|
|||||||
|
# ADR-0008:将 PostgreSQL Tenant Operator 合并为 Ayatori Database 模块
|
||||||
|
|
||||||
|
- 状态:Accepted
|
||||||
|
- 日期:2026-09-20
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
独立仓库 `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 保留入口。
|
||||||
+26
-27
@@ -9,53 +9,52 @@
|
|||||||
- 定义 API、conditions、ownership 和 executor 公共约定。
|
- 定义 API、conditions、ownership 和 executor 公共约定。
|
||||||
- 建立不可变制品与 Dev 到 Prod promotion。
|
- 建立不可变制品与 Dev 到 Prod promotion。
|
||||||
|
|
||||||
## 1. Job Service
|
## 1. Controller 纵向验证切片
|
||||||
|
|
||||||
- 实现最小 `Job` API。
|
- 使用当前最小 `Job` API 验证 watch、状态机、finalizer、取消、TTL、external reference 与
|
||||||
- Kubernetes Pod executor。
|
backend adapter。
|
||||||
- 统一日志、退出状态、超时、workspace、cache 与 artifact。
|
- Kubernetes executor 不能与 management API client 或同集群 namespace 语义绑定。
|
||||||
- 接入 Gitea Actions 和平台内部 IaC 执行。
|
- 验证完成后,将可复用机制收敛为内部 `Run`/execution 能力;不把这一切片扩展为 FaaS、
|
||||||
|
Cloud Run 或通用 Job Service。
|
||||||
|
|
||||||
## 2. OpenSandbox Executor
|
## 2. 首批资源产品
|
||||||
|
|
||||||
- 通过 OpenSandbox lifecycle 与 execd API 创建、执行和清理 sandbox。
|
- `Database`:PostgreSQL database、role、credential 与回收。
|
||||||
- 支持强隔离任务、未知代码、嵌套容器和 AI agent。
|
- `LoadBalancer`:Envoy 配置/xDS、健康检查、固定 VIP 与 GoBGP 路由宣告。
|
||||||
- 增加交互式 `Sandbox` API、TTL、endpoint 与 snapshot。
|
- `Bucket`:SeaweedFS bucket、policy、credential 与删除策略。
|
||||||
|
- 按纵向价值选择先后,不为三者预先建立统一 provider 框架。
|
||||||
|
|
||||||
## 3. Human Executor
|
## 3. Human Executor 与延迟自动化
|
||||||
|
|
||||||
- `ManualTask`、`TaskReport` 和版本化 Runbook。
|
- `ManualTask`、`TaskReport` 和版本化 Runbook。
|
||||||
- Telegram/Email 通知、领取、提醒和升级。
|
- Telegram/Email 通知、领取、提醒和升级。
|
||||||
- 后端验证与上游 reconcile 恢复。
|
- 后端验证与上游 reconcile 恢复。
|
||||||
|
|
||||||
## 4. LBaaS
|
## 4. Compute 与节点生命周期
|
||||||
|
|
||||||
- Envoy 配置/xDS adapter。
|
- 建立稳定的 `VirtualMachine` 北向 API,并支持现有资源 adopt。
|
||||||
- 健康检查与 GoBGP 路由宣告。
|
- 南向按能力组合 Proxmox API、节点受限 Agent/CLI 与 `ManualTask`,不假设 Proxmox API 完整。
|
||||||
- 固定 VIP、listener/backend 引用和故障恢复。
|
- Node 加入、drain 和 `SafeToRemove`;Node API 由 Ayatori Compute Agent 实现,不依赖 kubelet。
|
||||||
|
|
||||||
## 5. Compute 与节点生命周期
|
|
||||||
|
|
||||||
- Proxmox VM adapter 与现有资源 adopt。
|
|
||||||
- ComputeNode 加入、drain 和 `SafeToRemove`。
|
|
||||||
- StorageClass、StoragePool、Volume 与迁移计划。
|
- StorageClass、StoragePool、Volume 与迁移计划。
|
||||||
- 先支持人工磁盘迁移,再通过 Job executor 自动化。
|
- 先支持人工磁盘迁移,再按实际收益自动化。
|
||||||
|
|
||||||
## 6. 数据服务
|
## 5. 条件性扩展
|
||||||
|
|
||||||
- PostgreSQL database/role/credential。
|
- OpenSandbox/microVM 可以作为内部 Run backend,但不由此产生 FaaS 产品承诺。
|
||||||
- SeaweedFS bucket/policy/credential。
|
- DNS、证书和 Credential 只有在跨系统协调收益明确时形成独立资源。
|
||||||
- DNS 与证书资源。
|
- KaaS 只有出现托管控制面、租户隔离或频繁集群生命周期的真实需求时才立项。
|
||||||
|
|
||||||
## 7. KaaS
|
### KaaS 候选方案
|
||||||
|
|
||||||
- 采用成熟 hosted-control-plane 后端。
|
- 采用成熟 hosted-control-plane 后端。
|
||||||
- 组合控制面、worker、LB、DNS、网络和凭据。
|
- 组合控制面、worker、LB、DNS、网络和凭据。
|
||||||
- 用户集群只暴露 worker node,控制面完全由平台托管。
|
- 用户集群只暴露 worker node,控制面完全由平台托管。
|
||||||
|
- 本节记录候选实现边界,不构成路线图承诺。
|
||||||
|
|
||||||
## 首个业务里程碑
|
## 后续 Compute 验收场景
|
||||||
|
|
||||||
完成 Laptop Rebuild Readiness:
|
Database 等首批资源优先落地。Compute 开始实施后,以 Laptop Rebuild Readiness 验证节点
|
||||||
|
生命周期与恢复能力;该场景不作为首批 Database、LoadBalancer 或 Bucket 的交付前置条件:
|
||||||
|
|
||||||
1. 临时节点加入。
|
1. 临时节点加入。
|
||||||
2. laptop 上的 workload 被重建、迁移或形成可执行人工任务。
|
2. laptop 上的 workload 被重建、迁移或形成可执行人工任务。
|
||||||
|
|||||||
@@ -29,6 +29,20 @@ Ayatori 是具有产品质量的内部平台,而非初期即面向公众的通
|
|||||||
平台允许对当前环境形成明确意见:Proxmox、OpenSandbox、Envoy、GoBGP、OpenBao、
|
平台允许对当前环境形成明确意见:Proxmox、OpenSandbox、Envoy、GoBGP、OpenBao、
|
||||||
PostgreSQL、SeaweedFS、Samba AD DNS、Cloudflare 和 Flux 都可以是已知实现。
|
PostgreSQL、SeaweedFS、Samba AD DNS、Cloudflare 和 Flux 都可以是已知实现。
|
||||||
|
|
||||||
|
Ayatori 不以补齐传统私有云或公有云的产品目录为目标。一个资源只有同时满足以下条件,才进入
|
||||||
|
北向 API:
|
||||||
|
|
||||||
|
1. homelab 存在现实消费者和重复需求;
|
||||||
|
2. 现有后端 API 或 IaC 无法提供足够的管理体验;
|
||||||
|
3. 持续 observe/reconcile 明显优于一次性自动化;
|
||||||
|
4. 统一生命周期、状态、组合或权限能产生可验证的收益;
|
||||||
|
5. 收益足以承担长期 API 兼容、controller 和恢复测试成本。
|
||||||
|
|
||||||
|
当前最明确的管理缺口是 Database、LoadBalancer 与 Bucket/Object Storage。VirtualMachine 同样
|
||||||
|
具有明确价值:Proxmox 的 API 不能覆盖所需的全部生命周期,一部分操作必须在节点上通过 CLI
|
||||||
|
完成,因此 Ayatori 可以提供稳定北向 API,并在南向组合 Proxmox API、受限节点 Agent 与人工
|
||||||
|
任务。KaaS 只有在出现托管控制面的实际需求时才进入实现,不是产品路线的必达终点。
|
||||||
|
|
||||||
## 非目标
|
## 非目标
|
||||||
|
|
||||||
- 不替代 hypervisor、microVM runtime、数据库、对象存储或网络协议栈。
|
- 不替代 hypervisor、microVM runtime、数据库、对象存储或网络协议栈。
|
||||||
@@ -36,3 +50,5 @@ PostgreSQL、SeaweedFS、Samba AD DNS、Cloudflare 和 Flux 都可以是已知
|
|||||||
- 不以隐藏全部后端信息或制造虚假多云可移植性为目标。
|
- 不以隐藏全部后端信息或制造虚假多云可移植性为目标。
|
||||||
- 不创建理解所有应用需求的中央 Application controller。
|
- 不创建理解所有应用需求的中央 Application controller。
|
||||||
- 不要求所有人工步骤立即自动化。
|
- 不要求所有人工步骤立即自动化。
|
||||||
|
- 不因为已有 Run、OpenSandbox 或 microVM backend,就构建 FaaS、Cloud Run 或应用托管产品。
|
||||||
|
- 不预先承诺 KaaS;它是需求驱动的候选能力。
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
module git.ddupan.top/panxiao81/ayatori
|
||||||
|
|
||||||
|
go 1.27.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/jackc/pgx/v5 v5.11.0
|
||||||
|
github.com/jackc/tern/v2 v2.4.3
|
||||||
|
k8s.io/api v0.37.0
|
||||||
|
k8s.io/apimachinery v0.37.0
|
||||||
|
k8s.io/client-go v0.37.0
|
||||||
|
sigs.k8s.io/controller-runtime v0.25.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
cel.dev/expr v0.25.1 // indirect
|
||||||
|
dario.cat/mergo v1.0.2 // indirect
|
||||||
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
|
github.com/Masterminds/semver/v3 v3.5.0 // indirect
|
||||||
|
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||||
|
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
|
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||||
|
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
github.com/fxamacker/cbor/v2 v2.9.1 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/go-logr/zapr v1.3.0 // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v1.0.0 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v1.0.0 // indirect
|
||||||
|
github.com/go-openapi/swag v0.27.1 // indirect
|
||||||
|
github.com/go-openapi/swag/cmdutils v0.27.1 // indirect
|
||||||
|
github.com/go-openapi/swag/conv v0.27.1 // indirect
|
||||||
|
github.com/go-openapi/swag/fileutils v0.27.1 // indirect
|
||||||
|
github.com/go-openapi/swag/jsonutils v0.27.1 // indirect
|
||||||
|
github.com/go-openapi/swag/loading v0.27.1 // indirect
|
||||||
|
github.com/go-openapi/swag/mangling v0.27.1 // indirect
|
||||||
|
github.com/go-openapi/swag/netutils v0.27.1 // indirect
|
||||||
|
github.com/go-openapi/swag/pools v0.27.1 // indirect
|
||||||
|
github.com/go-openapi/swag/stringutils v0.27.1 // indirect
|
||||||
|
github.com/go-openapi/swag/typeutils v0.27.1 // indirect
|
||||||
|
github.com/go-openapi/swag/yamlutils v0.27.1 // indirect
|
||||||
|
github.com/google/cel-go v0.29.2 // indirect
|
||||||
|
github.com/google/gnostic-models v0.7.0 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
|
||||||
|
github.com/huandu/xstrings v1.5.0 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
|
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
|
github.com/prometheus/client_golang v1.24.0 // indirect
|
||||||
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
|
github.com/prometheus/common v0.70.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.21.1 // indirect
|
||||||
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
|
github.com/spf13/cobra v1.10.2 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.44.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||||
|
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
go.uber.org/zap v1.27.1 // indirect
|
||||||
|
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||||
|
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||||
|
golang.org/x/crypto v0.55.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
|
||||||
|
golang.org/x/net v0.57.0 // indirect
|
||||||
|
golang.org/x/oauth2 v0.36.0 // indirect
|
||||||
|
golang.org/x/sync v0.22.0 // indirect
|
||||||
|
golang.org/x/sys v0.47.0 // indirect
|
||||||
|
golang.org/x/term v0.45.0 // indirect
|
||||||
|
golang.org/x/text v0.41.0 // indirect
|
||||||
|
golang.org/x/time v0.15.0 // indirect
|
||||||
|
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||||
|
google.golang.org/grpc v1.82.1 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
|
||||||
|
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||||
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
|
k8s.io/apiextensions-apiserver v0.37.0 // indirect
|
||||||
|
k8s.io/apiserver v0.37.0 // indirect
|
||||||
|
k8s.io/component-base v0.37.0 // indirect
|
||||||
|
k8s.io/klog/v2 v2.140.0 // indirect
|
||||||
|
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad // indirect
|
||||||
|
k8s.io/streaming v0.37.0 // indirect
|
||||||
|
k8s.io/utils v0.0.0-20260626114624-be93311217bd // indirect
|
||||||
|
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.36.0 // indirect
|
||||||
|
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||||
|
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||||
|
sigs.k8s.io/structured-merge-diff/v6 v6.4.2 // indirect
|
||||||
|
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
||||||
|
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||||
|
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||||
|
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||||
|
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||||
|
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||||
|
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
|
||||||
|
github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||||
|
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
||||||
|
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||||
|
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
|
||||||
|
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
|
||||||
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||||
|
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
|
||||||
|
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||||
|
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
|
||||||
|
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
|
||||||
|
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
|
||||||
|
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
|
github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
|
||||||
|
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||||
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
|
||||||
|
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
|
||||||
|
github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s=
|
||||||
|
github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y=
|
||||||
|
github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY=
|
||||||
|
github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0=
|
||||||
|
github.com/go-openapi/swag v0.27.1 h1:VotvOLWW8q/EAxB0YdsBBGC8XYyeL1YwBj2ungAGPNg=
|
||||||
|
github.com/go-openapi/swag v0.27.1/go.mod h1:GTkJPwHfhJp6MWr4/rCh64HVI3Ofu+tcsbfjfHmTxpE=
|
||||||
|
github.com/go-openapi/swag/cmdutils v0.27.1 h1:I7sYqaWVl5mq0NEmNQkAmFDyNin9ufvMX/p2zwtQaOE=
|
||||||
|
github.com/go-openapi/swag/cmdutils v0.27.1/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM=
|
||||||
|
github.com/go-openapi/swag/conv v0.27.1 h1:8wi9ZG+olmY1wXphl93EWniPtbSPkXM/feH7FgjsvrU=
|
||||||
|
github.com/go-openapi/swag/conv v0.27.1/go.mod h1:QbqMivkpKhC3g1B1GGGOJ6ANewI3S62dbzYu3Duowqs=
|
||||||
|
github.com/go-openapi/swag/fileutils v0.27.1 h1:QQqBSoi5mW4XpU85nS0mLcA+zAE6vLzrb0QkmLKf9oM=
|
||||||
|
github.com/go-openapi/swag/fileutils v0.27.1/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8=
|
||||||
|
github.com/go-openapi/swag/jsonutils v0.27.1 h1:SVgK3i4USzCU5mibOOS/l4ea2h9UQXy7J7RNLTjuXjU=
|
||||||
|
github.com/go-openapi/swag/jsonutils v0.27.1/go.mod h1:tdlEpZqdcQ17uj6J4YdK9vd8It5qWMwjWXOs0tjpRlk=
|
||||||
|
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.1 h1:mJu3COL9WEaZVp/Kf2PRMi7tPszPEJfSr/OO75ynCs8=
|
||||||
|
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.1/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY=
|
||||||
|
github.com/go-openapi/swag/loading v0.27.1 h1:/DxUgDXKbBX4bcn7r9uEXfJyzN5XpiJmZplzQTjrRCY=
|
||||||
|
github.com/go-openapi/swag/loading v0.27.1/go.mod h1:jvGh3iA2+zyUUycB5fgJWzeHnhrpvGnJJM0RVE9ZShE=
|
||||||
|
github.com/go-openapi/swag/mangling v0.27.1 h1:yC9D0HyUE8gbP+BfmGx9+AA89ikwZTMjESK3OnnoaqA=
|
||||||
|
github.com/go-openapi/swag/mangling v0.27.1/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w=
|
||||||
|
github.com/go-openapi/swag/netutils v0.27.1 h1:mICMFoS82F5TZ4Zy3cqmcQk+BFeCp3Uyq3Np7GI0/qU=
|
||||||
|
github.com/go-openapi/swag/netutils v0.27.1/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ=
|
||||||
|
github.com/go-openapi/swag/pools v0.27.1 h1:9LeadcMyb2GJCbXX5hVQDbZ2Lq9TL4dCs/nx1j5DO0E=
|
||||||
|
github.com/go-openapi/swag/pools v0.27.1/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE=
|
||||||
|
github.com/go-openapi/swag/stringutils v0.27.1 h1:ZXePZ0r2p1qSjo8tD3Un4vFj8+FqlCkczxDrJIhYUp8=
|
||||||
|
github.com/go-openapi/swag/stringutils v0.27.1/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM=
|
||||||
|
github.com/go-openapi/swag/typeutils v0.27.1 h1:KSTdFlfnse4r6dP9IrEnwMldjE+zs71UeEB3//PtVXc=
|
||||||
|
github.com/go-openapi/swag/typeutils v0.27.1/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ=
|
||||||
|
github.com/go-openapi/swag/yamlutils v0.27.1 h1:ftxv6xvXb1E3zohUc+okZ9nSqNb9StQX/FXnKZ98sQA=
|
||||||
|
github.com/go-openapi/swag/yamlutils v0.27.1/go.mod h1:bnxFIB1qewGRiZHypXGZ3fNgf13/0HfRgnS/iZBDrOo=
|
||||||
|
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0=
|
||||||
|
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo=
|
||||||
|
github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug=
|
||||||
|
github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/google/cel-go v0.29.2 h1:ZtDxkeiMmz0mxbKDYiNkE5Lk7V5edMRcaaDf2jX002k=
|
||||||
|
github.com/google/cel-go v0.29.2/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
|
||||||
|
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||||
|
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||||
|
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
|
||||||
|
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
|
||||||
|
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||||
|
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg=
|
||||||
|
github.com/jackc/pgx/v5 v5.11.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/jackc/tern/v2 v2.4.3 h1:g293d3OZgW7OFEhsYXgEv0C21jea2boNr0VR4K5I7OY=
|
||||||
|
github.com/jackc/tern/v2 v2.4.3/go.mod h1:rMpMuRYcff5wWLptoTSO1qcDxJ4OodysvK17i2SVBys=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ=
|
||||||
|
github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
|
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||||
|
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||||
|
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||||
|
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||||
|
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.27.4 h1:fcEcQW/A++6aZAZQNUmNjvA9PSOzefMJBerHJ4t8v8Y=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.27.4/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
||||||
|
github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q=
|
||||||
|
github.com/onsi/gomega v1.39.0/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prometheus/client_golang v1.24.0 h1:5XStIklKuAtJSNpdD3s8XJj/Yv78IQmE1kbNk87JrAI=
|
||||||
|
github.com/prometheus/client_golang v1.24.0/go.mod h1:QcsNdotprC2nS4BTM2ucbcqxd2CeXTEa9jW7zHO9iDE=
|
||||||
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
|
github.com/prometheus/common v0.70.0 h1:bcpru3tWPVnxGnETLgOV5jbp/JRXgYEyv65CuBLAMMI=
|
||||||
|
github.com/prometheus/common v0.70.0/go.mod h1:S/SFasQmgGiYH6C81LKCtYa8QACgthGg5zxL2udV7SY=
|
||||||
|
github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
|
||||||
|
github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||||
|
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||||
|
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||||
|
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||||
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||||
|
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||||
|
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||||
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
|
||||||
|
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||||
|
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 h1:qazEJlUOQzhCpzQpFETGby7EdqjI1wsd0W+6Gg1SCTU=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0/go.mod h1:fOD2Yefuxixkx3ahVNf0O/PERb6r4OlbxfATVnYvzCo=
|
||||||
|
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||||
|
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
|
||||||
|
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||||
|
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||||
|
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||||
|
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
|
||||||
|
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
|
||||||
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||||
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||||
|
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
||||||
|
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
|
||||||
|
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||||
|
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||||
|
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||||
|
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||||
|
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||||
|
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||||
|
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||||
|
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||||
|
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||||
|
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||||
|
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||||
|
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||||
|
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
|
||||||
|
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
|
||||||
|
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
|
||||||
|
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
||||||
|
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||||
|
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
|
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
|
||||||
|
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
|
||||||
|
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
|
||||||
|
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
|
||||||
|
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||||
|
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||||
|
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
k8s.io/api v0.37.0 h1:Z//Vj9N7RA/yS2sDmxyeo7h+RR4zbUrd2vrd3Z0TbB4=
|
||||||
|
k8s.io/api v0.37.0/go.mod h1:LKXgcJWMc+f4OLbP5SFR8rulEg07zZhpi/zMULiBImk=
|
||||||
|
k8s.io/apiextensions-apiserver v0.37.0 h1:zRMQ3+/LIE5oZ0tVvXwYHC+dIkSP5cjNWju7AZU1LOI=
|
||||||
|
k8s.io/apiextensions-apiserver v0.37.0/go.mod h1:HU0PfSBwchHL5iDau6jjt9zU6ryWkDDlaVUiq91NK80=
|
||||||
|
k8s.io/apimachinery v0.37.0 h1:Np2AbDtf8x6RDHiD8T9LbKJ9gaegeVNa8yNm5FuGKm0=
|
||||||
|
k8s.io/apimachinery v0.37.0/go.mod h1:RN3nhprFSCxOi5Selxd7oMTXOe/c+ZbcE7Im+TS2zkE=
|
||||||
|
k8s.io/apiserver v0.37.0 h1:TXg7OxsOWrAH8J4Zi/gBAZuMw1Dfdd+6cca2h4qjRqo=
|
||||||
|
k8s.io/apiserver v0.37.0/go.mod h1:OddHDF4gy9qyIb8o/3+qaeP6S0vEObWLgOygVqXksv0=
|
||||||
|
k8s.io/client-go v0.37.0 h1:nsN31fy8wBySuZ+QRnKmrjRSQLOG2rvoGN0tKd12zhQ=
|
||||||
|
k8s.io/client-go v0.37.0/go.mod h1:FcGqw+Ll/gNQiq+nPGY1Oyt9y7SgDh1d3MW3RFDEbn0=
|
||||||
|
k8s.io/component-base v0.37.0 h1:3SdSa4+itMdFTDFTeR8CxKGmSTSMXFlKL4ky8OqjguM=
|
||||||
|
k8s.io/component-base v0.37.0/go.mod h1:LjOebp4R9y6LODWZQv102ZQxGheLcDO2ZJLAw6bbh4I=
|
||||||
|
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
|
||||||
|
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
|
||||||
|
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad h1:oXImqH8mQNk7PmvzKhmN3ddJoY6OnyM225MXwGHPm0A=
|
||||||
|
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad/go.mod h1:0/mqHCVhlumdJ3BhCfnjSZQE037nAhNodh1/hK0T8/I=
|
||||||
|
k8s.io/streaming v0.37.0 h1:iPBUZLZiKt5bV+lxJurASMOV07VuBhNpiwJt2//AWrM=
|
||||||
|
k8s.io/streaming v0.37.0/go.mod h1:APlJR26ZWRcVy5bIEj0QRrKUXROtBHPcxl2NT7EAzPU=
|
||||||
|
k8s.io/utils v0.0.0-20260626114624-be93311217bd h1:Ea7fgQ5we8Y9T0OX5o0dAHzQOBRI07D/dEYRaB9ZZEs=
|
||||||
|
k8s.io/utils v0.0.0-20260626114624-be93311217bd/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
||||||
|
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.36.0 h1:/YpDJ4vReG7ZmzSpBGxduXgywWkJU9zHubgJG03MT+Y=
|
||||||
|
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.36.0/go.mod h1:tJo1aepTXyR+8Xs3sUsGBDk4Ub2AM5dPAPKJx0mpm5c=
|
||||||
|
sigs.k8s.io/controller-runtime v0.25.0 h1:44KgRUPew331KSJpNu8zJow3iTR5W0p/SfrHdw3lV40=
|
||||||
|
sigs.k8s.io/controller-runtime v0.25.0/go.mod h1:4QqLdT6z/L6Olj8JJCtvztid4/fnIiYsfaTFScegctc=
|
||||||
|
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||||
|
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||||
|
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||||
|
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||||
|
sigs.k8s.io/structured-merge-diff/v6 v6.4.2 h1:qdOxHwrl2Kaag1aQEarlYcOA9vSyGCp3CIki3aW8c4Q=
|
||||||
|
sigs.k8s.io/structured-merge-diff/v6 v6.4.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||||
|
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||||
|
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Package kubernetes 提供 Database 所需的 Kubernetes API 薄适配。
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation"
|
||||||
|
typedcore "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
|
|
||||||
|
"git.ddupan.top/panxiao81/ayatori/internal/database/application"
|
||||||
|
"git.ddupan.top/panxiao81/ayatori/internal/database/domain/instance"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SecretCredentials 直接读取 API server,不将 Secret 数据纳入共享 informer cache。
|
||||||
|
// namespace 在装配时固定,Instance 不能选择跨 namespace 读取。
|
||||||
|
type SecretCredentials struct {
|
||||||
|
secrets typedcore.SecretInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSecretCredentials(config *rest.Config, namespace string) (*SecretCredentials, error) {
|
||||||
|
if config == nil || len(validation.IsDNS1123Label(namespace)) != 0 {
|
||||||
|
return nil, errors.New("valid controller namespace and API configuration required")
|
||||||
|
}
|
||||||
|
client, err := typedcore.NewForConfig(config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, application.ErrCredentialsUnavailable
|
||||||
|
}
|
||||||
|
return &SecretCredentials{secrets: client.Secrets(namespace)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SecretCredentials) Read(ctx context.Context, ref instance.CredentialReference) (application.Credentials, error) {
|
||||||
|
if err := ref.Validate(); err != nil {
|
||||||
|
return application.Credentials{}, application.ErrCredentialsInvalid
|
||||||
|
}
|
||||||
|
keys := ref.Values()
|
||||||
|
secret, err := r.secrets.Get(ctx, keys.Name, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return application.Credentials{}, application.ErrCredentialsUnavailable
|
||||||
|
}
|
||||||
|
return decode(secret, keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(secret *corev1.Secret, keys instance.CredentialReferenceValues) (application.Credentials, error) {
|
||||||
|
if secret.DeletionTimestamp != nil {
|
||||||
|
return application.Credentials{}, application.ErrCredentialsUnavailable
|
||||||
|
}
|
||||||
|
return application.NewCredentials(string(secret.Data[keys.UsernameKey]), string(secret.Data[keys.PasswordKey]))
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Package postgresql 使用 pgxpool 提供 PostgreSQL 能力的薄适配。
|
||||||
|
package postgresql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
|
||||||
|
"git.ddupan.top/panxiao81/ayatori/internal/database/application"
|
||||||
|
"git.ddupan.top/panxiao81/ayatori/internal/database/domain/instance"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Connector 不读取 Secret、不决定连接何时替换;池本身由 pgxpool 实现。
|
||||||
|
type Connector struct {
|
||||||
|
RootCert string
|
||||||
|
}
|
||||||
|
|
||||||
|
type database struct {
|
||||||
|
pool *pgxpool.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*database) String() string { return "[redacted PostgreSQL database]" }
|
||||||
|
func (d *database) GoString() string { return d.String() }
|
||||||
|
func (d *database) Close() {
|
||||||
|
d.pool.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *database) Version(ctx context.Context) (string, error) {
|
||||||
|
var version string
|
||||||
|
if err := d.pool.QueryRow(ctx, "SHOW server_version").Scan(&version); err != nil {
|
||||||
|
return "", safeError(err, application.ErrObservation)
|
||||||
|
}
|
||||||
|
return version, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Connector) Connect(ctx context.Context, endpoint instance.Endpoint, credentials application.Credentials) (application.Database, error) {
|
||||||
|
if err := endpoint.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if credentials.Username() == "" || credentials.Password() == "" {
|
||||||
|
return nil, application.ErrCredentialsInvalid
|
||||||
|
}
|
||||||
|
endpointValues := endpoint.Values()
|
||||||
|
query := url.Values{
|
||||||
|
"sslmode": {string(endpointValues.TLSMode)},
|
||||||
|
"connect_timeout": {"5"},
|
||||||
|
"application_name": {"ayatori-database-management"},
|
||||||
|
}
|
||||||
|
if c.RootCert != "" {
|
||||||
|
query.Set("sslrootcert", c.RootCert)
|
||||||
|
}
|
||||||
|
connectionURL := url.URL{
|
||||||
|
Scheme: "postgresql",
|
||||||
|
Host: net.JoinHostPort(endpointValues.Host, strconv.Itoa(endpointValues.Port)),
|
||||||
|
Path: "/" + endpointValues.ManagementDatabase,
|
||||||
|
User: url.UserPassword(credentials.Username(), credentials.Password()),
|
||||||
|
RawQuery: query.Encode(),
|
||||||
|
}
|
||||||
|
config, err := pgxpool.ParseConfig(connectionURL.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, application.ErrConnection
|
||||||
|
}
|
||||||
|
// pgx 不实现 libpq hostaddr;复用其 LookupFunc 扩展点,TLS 验证身份仍采用 host。
|
||||||
|
config.ConnConfig.LookupFunc = func(context.Context, string) ([]string, error) {
|
||||||
|
return []string{endpointValues.HostAddr}, nil
|
||||||
|
}
|
||||||
|
config.ConnConfig.Fallbacks = nil
|
||||||
|
pool, err := pgxpool.NewWithConfig(ctx, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, safeError(err, application.ErrConnection)
|
||||||
|
}
|
||||||
|
if err := pool.Ping(ctx); err != nil {
|
||||||
|
pool.Close()
|
||||||
|
return nil, safeError(err, application.ErrConnection)
|
||||||
|
}
|
||||||
|
return &database{pool: pool}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func safeError(err, fallback error) error {
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
return context.Canceled
|
||||||
|
}
|
||||||
|
if errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
return context.DeadlineExceeded
|
||||||
|
}
|
||||||
|
var pgerr *pgconn.PgError
|
||||||
|
if errors.As(err, &pgerr) && (pgerr.Code == "28P01" || pgerr.Code == "28000") {
|
||||||
|
return application.ErrAuthentication
|
||||||
|
}
|
||||||
|
if _, ok := errors.AsType[*tls.CertificateVerificationError](err); ok {
|
||||||
|
return application.ErrAuthentication
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
//go:build integration
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2026.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package postgresql_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os/exec"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
|
"git.ddupan.top/panxiao81/ayatori/internal/database/adapter/postgresql"
|
||||||
|
"git.ddupan.top/panxiao81/ayatori/internal/database/application"
|
||||||
|
"git.ddupan.top/panxiao81/ayatori/internal/database/domain/instance"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestManagementSecretScopeAndMissingDependencyRecovery(t *testing.T) {
|
||||||
|
fixture := newCredentialFixture(t)
|
||||||
|
reference := fixture.target.Definition().AdminCredential()
|
||||||
|
|
||||||
|
fixture.createSecret(t, "unrelated")
|
||||||
|
if _, err := fixture.reader.Read(fixture.ctx, reference); !errors.Is(err, application.ErrCredentialsUnavailable) {
|
||||||
|
t.Fatal("a Secret in another namespace satisfied the reference")
|
||||||
|
}
|
||||||
|
if _, err := fixture.service.ObserveVersion(fixture.ctx, fixture.target); !errors.Is(err, application.ErrCredentialsUnavailable) {
|
||||||
|
t.Fatal("missing Secret did not fail closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
fixture.createSecret(t, controllerNamespace)
|
||||||
|
if _, err := fixture.deniedReader.Read(fixture.ctx, reference); !errors.Is(err, application.ErrCredentialsUnavailable) {
|
||||||
|
t.Fatal("API server did not enforce Secret RBAC")
|
||||||
|
}
|
||||||
|
fixture.observeVersion(t)
|
||||||
|
|
||||||
|
fixture.updateSecret(t, func(secret *corev1.Secret) {
|
||||||
|
delete(secret.Data, "credential")
|
||||||
|
})
|
||||||
|
if _, err := fixture.service.ObserveVersion(fixture.ctx, fixture.target); !errors.Is(err, application.ErrCredentialsInvalid) {
|
||||||
|
t.Fatal("missing credential field reused a cached connection")
|
||||||
|
}
|
||||||
|
fixture.updateSecret(t, func(secret *corev1.Secret) {
|
||||||
|
secret.Data["credential"] = []byte(fixturePassword)
|
||||||
|
})
|
||||||
|
fixture.observeVersion(t)
|
||||||
|
|
||||||
|
err := fixture.client.CoreV1().Secrets(controllerNamespace).Delete(fixture.ctx, secretName, metav1.DeleteOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("cannot delete fixture Secret")
|
||||||
|
}
|
||||||
|
if _, err := fixture.service.ObserveVersion(fixture.ctx, fixture.target); !errors.Is(err, application.ErrCredentialsUnavailable) {
|
||||||
|
t.Fatal("deleted Secret retained access")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEffectiveCredentialChangesReplaceConnection(t *testing.T) {
|
||||||
|
fixture := newCredentialFixture(t)
|
||||||
|
fixture.createSecret(t, controllerNamespace)
|
||||||
|
fixture.observeVersion(t)
|
||||||
|
originalBackend := fixture.backendIDs(t)
|
||||||
|
if originalBackend == "" {
|
||||||
|
t.Fatal("management connection not visible in PostgreSQL")
|
||||||
|
}
|
||||||
|
|
||||||
|
fixture.updateSecret(t, func(secret *corev1.Secret) {
|
||||||
|
secret.Labels = map[string]string{"changed": "true"}
|
||||||
|
secret.Data["unrelated"] = []byte("ignored")
|
||||||
|
})
|
||||||
|
fixture.observeVersion(t)
|
||||||
|
if fixture.backendIDs(t) != originalBackend {
|
||||||
|
t.Fatal("metadata or unrelated fields rebuilt the connection")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先改变 Secret、暂不改变服务器密码:旧连接必须失效,新认证必须失败。
|
||||||
|
fixture.updateSecret(t, func(secret *corev1.Secret) {
|
||||||
|
secret.Data["credential"] = []byte(rotatedPassword)
|
||||||
|
})
|
||||||
|
version, err := fixture.service.ObserveVersion(fixture.ctx, fixture.target)
|
||||||
|
if !errors.Is(err, application.ErrAuthentication) || version != "" {
|
||||||
|
t.Fatal("old connection bypassed changed credentials")
|
||||||
|
}
|
||||||
|
|
||||||
|
fixture.queryPostgres(t, "ALTER ROLE postgres PASSWORD '"+rotatedPassword+"'")
|
||||||
|
fixture.observeVersion(t)
|
||||||
|
if fixture.backendIDs(t) == originalBackend {
|
||||||
|
t.Fatal("password rotation reused the old backend")
|
||||||
|
}
|
||||||
|
|
||||||
|
fixture.updateSecret(t, func(secret *corev1.Secret) {
|
||||||
|
secret.Data["login"] = []byte("nonexistent")
|
||||||
|
})
|
||||||
|
if _, err := fixture.service.ObserveVersion(fixture.ctx, fixture.target); !errors.Is(err, application.ErrAuthentication) {
|
||||||
|
t.Fatal("username change did not require a new authentication")
|
||||||
|
}
|
||||||
|
fixture.updateSecret(t, func(secret *corev1.Secret) {
|
||||||
|
secret.Data["login"] = []byte(fixtureUser)
|
||||||
|
})
|
||||||
|
fixture.observeVersion(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestObservationDiscardsResultWhenCredentialsChange(t *testing.T) {
|
||||||
|
fixture := newCredentialFixture(t)
|
||||||
|
fixture.createSecret(t, controllerNamespace)
|
||||||
|
|
||||||
|
reads := 0
|
||||||
|
fixture.gate.beforeRead = func() {
|
||||||
|
reads++
|
||||||
|
if reads == 2 {
|
||||||
|
fixture.updateSecret(t, func(secret *corev1.Secret) {
|
||||||
|
secret.Data["credential"] = []byte(rotatedPassword)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
version, err := fixture.service.ObserveVersion(fixture.ctx, fixture.target)
|
||||||
|
if !errors.Is(err, application.ErrCredentialsChanged) {
|
||||||
|
t.Fatal("in-flight rotation was not detected")
|
||||||
|
}
|
||||||
|
if version != "" {
|
||||||
|
t.Fatal("observation returned data obtained with stale credentials")
|
||||||
|
}
|
||||||
|
if fixture.backendIDs(t) != "" {
|
||||||
|
t.Fatal("stale connection was retained after rotation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConnectionReleaseAndServiceRestart(t *testing.T) {
|
||||||
|
fixture := newCredentialFixture(t)
|
||||||
|
fixture.createSecret(t, controllerNamespace)
|
||||||
|
fixture.observeVersion(t)
|
||||||
|
|
||||||
|
fixture.service.Forget(fixture.target.Identity().Name())
|
||||||
|
if fixture.backendIDs(t) != "" {
|
||||||
|
t.Fatal("Forget retained a connection")
|
||||||
|
}
|
||||||
|
fixture.observeVersion(t)
|
||||||
|
|
||||||
|
fixture.service.Close()
|
||||||
|
fixture.service.Close()
|
||||||
|
if _, err := fixture.service.ObserveVersion(fixture.ctx, fixture.target); !errors.Is(err, application.ErrClosed) {
|
||||||
|
t.Fatal("closed service accepted work")
|
||||||
|
}
|
||||||
|
|
||||||
|
restarted, err := application.NewInstanceService(fixture.reader, postgresql.Connector{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(restarted.Close)
|
||||||
|
if _, err := restarted.ObserveVersion(fixture.ctx, fixture.target); err != nil {
|
||||||
|
t.Fatal("new service could not recover from stored Secret", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 此 fixture 未启用 TLS;各加密模式均不得偷偷回退到明文连接。
|
||||||
|
for _, mode := range []instance.TLSMode{instance.TLSRequire, instance.TLSVerifyCA, instance.TLSVerifyFull} {
|
||||||
|
securedTarget := target(t, fixture.port, mode)
|
||||||
|
if _, err := restarted.ObserveVersion(fixture.ctx, securedTarget); err == nil {
|
||||||
|
t.Fatal("TLS policy silently downgraded to plaintext")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConcurrentVersionObservations(t *testing.T) {
|
||||||
|
fixture := newCredentialFixture(t)
|
||||||
|
fixture.createSecret(t, controllerNamespace)
|
||||||
|
|
||||||
|
var workers sync.WaitGroup
|
||||||
|
for range 4 {
|
||||||
|
workers.Go(func() {
|
||||||
|
version, err := fixture.service.ObserveVersion(fixture.ctx, fixture.target)
|
||||||
|
if err != nil || version == "" {
|
||||||
|
t.Error("concurrent observation failed", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
workers.Go(func() {
|
||||||
|
fixture.service.Forget(fixture.target.Identity().Name())
|
||||||
|
})
|
||||||
|
workers.Wait()
|
||||||
|
fixture.observeVersion(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManagementConnectionRecoversAfterTimeout(t *testing.T) {
|
||||||
|
fixture := newCredentialFixture(t)
|
||||||
|
fixture.createSecret(t, controllerNamespace)
|
||||||
|
fixture.observeVersion(t)
|
||||||
|
|
||||||
|
if err := exec.CommandContext(fixture.ctx, "docker", "pause", fixture.containerID).Run(); err != nil {
|
||||||
|
t.Fatal("cannot pause isolated PostgreSQL fixture")
|
||||||
|
}
|
||||||
|
// 即使断言失败,也先恢复容器,再由 fixture 按原 ID 清理。
|
||||||
|
t.Cleanup(func() {
|
||||||
|
cleanupContext, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
_ = exec.CommandContext(cleanupContext, "docker", "unpause", fixture.containerID).Run()
|
||||||
|
})
|
||||||
|
|
||||||
|
queryContext, cancel := context.WithTimeout(fixture.ctx, 500*time.Millisecond)
|
||||||
|
version, err := fixture.service.ObserveVersion(queryContext, fixture.target)
|
||||||
|
cancel()
|
||||||
|
if err == nil || version != "" {
|
||||||
|
t.Fatal("timed out PostgreSQL observation returned a successful result")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := exec.CommandContext(fixture.ctx, "docker", "unpause", fixture.containerID).Run(); err != nil {
|
||||||
|
t.Fatal("cannot resume isolated PostgreSQL fixture")
|
||||||
|
}
|
||||||
|
fixture.observeVersion(t)
|
||||||
|
}
|
||||||
@@ -0,0 +1,332 @@
|
|||||||
|
//go:build integration
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2026.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package postgresql_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
||||||
|
|
||||||
|
secretadapter "git.ddupan.top/panxiao81/ayatori/internal/database/adapter/kubernetes"
|
||||||
|
"git.ddupan.top/panxiao81/ayatori/internal/database/adapter/postgresql"
|
||||||
|
"git.ddupan.top/panxiao81/ayatori/internal/database/application"
|
||||||
|
"git.ddupan.top/panxiao81/ayatori/internal/database/domain/instance"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
fixtureHost = "fixture.invalid"
|
||||||
|
fixtureUser = "postgres"
|
||||||
|
dockerExec = "exec"
|
||||||
|
fixtureImage = "postgres@sha256:18cfe3ef5e6815560c98237d6216d1e5119702fb0f3894c8785dd58b8bbe5d73"
|
||||||
|
fixturePassword = "AYATORI-TEST-ONLY-initial-password"
|
||||||
|
rotatedPassword = "AYATORI-TEST-ONLY-rotated-password"
|
||||||
|
controllerNamespace = "database-controller"
|
||||||
|
secretName = "management"
|
||||||
|
)
|
||||||
|
|
||||||
|
// fixture 不接受外部 DSN,只创建自己的临时容器并按确切 ID 清理。
|
||||||
|
func postgresFixture(t *testing.T, ctx context.Context) (string, int) {
|
||||||
|
t.Helper()
|
||||||
|
output, err := exec.CommandContext(ctx, "docker", "run", "--rm", "-d", "-p", "127.0.0.1::5432",
|
||||||
|
"-e", "POSTGRES_PASSWORD="+fixturePassword, fixtureImage).Output()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cannot start isolated PostgreSQL fixture: %s", fixtureCommandError(err))
|
||||||
|
}
|
||||||
|
id := strings.TrimSpace(string(output))
|
||||||
|
if !regexp.MustCompile(`^[a-f0-9]{64}$`).MatchString(id) {
|
||||||
|
t.Fatal("unexpected container identifier")
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
cleanup, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if exec.CommandContext(cleanup, "docker", "rm", "-f", id).Run() != nil {
|
||||||
|
t.Error("fixture cleanup failed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
output, err = exec.CommandContext(ctx, "docker", "inspect", "--format", `{{(index (index .NetworkSettings.Ports "5432/tcp") 0).HostPort}}`, id).Output()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cannot inspect fixture port: %s", fixtureCommandError(err))
|
||||||
|
}
|
||||||
|
port, err := strconv.Atoi(strings.TrimSpace(string(output)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("invalid fixture port")
|
||||||
|
}
|
||||||
|
// 初次 init 的临时服务器只监听 Unix socket,必须等最终 TCP listener。
|
||||||
|
for exec.CommandContext(ctx, "docker", dockerExec, id, "pg_isready", "-h", "127.0.0.1", "-U", fixtureUser).Run() != nil {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
t.Fatal("fixture startup timed out")
|
||||||
|
case <-time.After(200 * time.Millisecond):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return id, port
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output 将 stderr 保存在 ExitError 中;保留诊断,但不打印命令参数和测试密码。
|
||||||
|
func fixtureCommandError(err error) string {
|
||||||
|
detail := err.Error()
|
||||||
|
if exitErr, ok := errors.AsType[*exec.ExitError](err); ok {
|
||||||
|
detail += ": " + strings.TrimSpace(string(exitErr.Stderr))
|
||||||
|
}
|
||||||
|
redactor := strings.NewReplacer(
|
||||||
|
fixturePassword, "[REDACTED]",
|
||||||
|
rotatedPassword, "[REDACTED]",
|
||||||
|
)
|
||||||
|
return redactor.Replace(detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFixtureCommandErrorPreservesDiagnosticsAndRedactsPasswords(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "missing docker executable",
|
||||||
|
err: &exec.Error{Name: "docker", Err: exec.ErrNotFound},
|
||||||
|
want: "executable file not found",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "daemon failure from stderr",
|
||||||
|
err: &exec.ExitError{Stderr: []byte("Cannot connect to the Docker daemon")},
|
||||||
|
want: "Cannot connect to the Docker daemon",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "passwords in stderr",
|
||||||
|
err: &exec.ExitError{Stderr: []byte("failure: " + fixturePassword + " " + rotatedPassword)},
|
||||||
|
want: "failure: [REDACTED] [REDACTED]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "password in error text",
|
||||||
|
err: errors.New("failure: " + fixturePassword),
|
||||||
|
want: "failure: [REDACTED]",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
detail := fixtureCommandError(tt.err)
|
||||||
|
if !strings.Contains(detail, tt.want) {
|
||||||
|
t.Fatalf("diagnostic lost expected information: %q", tt.want)
|
||||||
|
}
|
||||||
|
if strings.Contains(detail, fixturePassword) || strings.Contains(detail, rotatedPassword) {
|
||||||
|
t.Fatal("diagnostic exposed a fixture password")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func target(t *testing.T, port int, mode instance.TLSMode) instance.ObservationTarget {
|
||||||
|
t.Helper()
|
||||||
|
id, err := instance.NewIdentity("fixture-uid", "fixture")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
revision, err := instance.NewRevision(1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
endpoint, err := instance.NewEndpoint(instance.EndpointValues{
|
||||||
|
Host: fixtureHost,
|
||||||
|
HostAddr: "127.0.0.1",
|
||||||
|
Port: port,
|
||||||
|
ManagementDatabase: fixtureUser,
|
||||||
|
TLSMode: mode,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ref, err := instance.NewCredentialReference(instance.CredentialReferenceValues{
|
||||||
|
Name: secretName,
|
||||||
|
UsernameKey: "login",
|
||||||
|
PasswordKey: "credential",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
definition, err := instance.NewDefinition(endpoint, ref)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
value, err := instance.NewObservationTarget(id, revision, definition)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在真实读取前设置屏障,确定性验证观测期间 Secret 变化;实际数据仍来自 API server。
|
||||||
|
type gatedReader struct {
|
||||||
|
application.CredentialReader
|
||||||
|
beforeRead func()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *gatedReader) Read(ctx context.Context, ref instance.CredentialReference) (application.Credentials, error) {
|
||||||
|
if r.beforeRead != nil {
|
||||||
|
r.beforeRead()
|
||||||
|
}
|
||||||
|
return r.CredentialReader.Read(ctx, ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
// credentialFixture 为每个场景创建独立 API server、PostgreSQL 和应用服务。
|
||||||
|
type credentialFixture struct {
|
||||||
|
ctx context.Context
|
||||||
|
client *kubernetes.Clientset
|
||||||
|
reader *secretadapter.SecretCredentials
|
||||||
|
deniedReader *secretadapter.SecretCredentials
|
||||||
|
gate *gatedReader
|
||||||
|
service *application.InstanceService
|
||||||
|
target instance.ObservationTarget
|
||||||
|
containerID string
|
||||||
|
port int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCredentialFixture(t *testing.T) *credentialFixture {
|
||||||
|
t.Helper()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
|
environment := &envtest.Environment{}
|
||||||
|
config, err := environment.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("envtest startup failed", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := environment.Stop(); err != nil {
|
||||||
|
t.Error("envtest cleanup failed", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
client, err := kubernetes.NewForConfig(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("cannot create test client")
|
||||||
|
}
|
||||||
|
for _, namespace := range []string{controllerNamespace, "unrelated"} {
|
||||||
|
_, err := client.CoreV1().Namespaces().Create(
|
||||||
|
ctx,
|
||||||
|
&corev1.Namespace{Name: namespace},
|
||||||
|
metav1.CreateOptions{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("cannot create fixture namespace")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := secretadapter.NewSecretCredentials(config, controllerNamespace)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
user, err := environment.AddUser(envtest.User{Name: "without-secret-access"}, config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
deniedReader, err := secretadapter.NewSecretCredentials(user.Config(), controllerNamespace)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
containerID, port := postgresFixture(t, ctx)
|
||||||
|
gate := &gatedReader{CredentialReader: reader}
|
||||||
|
service, err := application.NewInstanceService(gate, postgresql.Connector{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(service.Close)
|
||||||
|
|
||||||
|
return &credentialFixture{
|
||||||
|
ctx: ctx,
|
||||||
|
client: client,
|
||||||
|
reader: reader,
|
||||||
|
deniedReader: deniedReader,
|
||||||
|
gate: gate,
|
||||||
|
service: service,
|
||||||
|
target: target(t, port, instance.TLSDisable),
|
||||||
|
containerID: containerID,
|
||||||
|
port: port,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *credentialFixture) createSecret(t *testing.T, namespace string) {
|
||||||
|
t.Helper()
|
||||||
|
secret := &corev1.Secret{
|
||||||
|
Name: secretName,
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"login": []byte(fixtureUser),
|
||||||
|
"credential": []byte(fixturePassword),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if _, err := f.client.CoreV1().Secrets(namespace).Create(f.ctx, secret, metav1.CreateOptions{}); err != nil {
|
||||||
|
t.Fatal("cannot create fixture Secret")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *credentialFixture) updateSecret(t *testing.T, change func(*corev1.Secret)) {
|
||||||
|
t.Helper()
|
||||||
|
secrets := f.client.CoreV1().Secrets(controllerNamespace)
|
||||||
|
secret, err := secrets.Get(f.ctx, secretName, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("cannot read fixture Secret")
|
||||||
|
}
|
||||||
|
change(secret)
|
||||||
|
if _, err := secrets.Update(f.ctx, secret, metav1.UpdateOptions{}); err != nil {
|
||||||
|
t.Fatal("cannot update fixture Secret")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *credentialFixture) observeVersion(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
version, err := f.service.ObserveVersion(f.ctx, f.target)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("version observation failed", err)
|
||||||
|
}
|
||||||
|
if version == "" {
|
||||||
|
t.Fatal("successful observation returned an empty version")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *credentialFixture) queryPostgres(t *testing.T, sql string) string {
|
||||||
|
t.Helper()
|
||||||
|
output, err := exec.CommandContext(
|
||||||
|
f.ctx, "docker", dockerExec, f.containerID,
|
||||||
|
"psql", "-U", fixtureUser, "-tAc", sql,
|
||||||
|
).Output()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("fixture SQL failed")
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *credentialFixture) backendIDs(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
return f.queryPostgres(t, `
|
||||||
|
SELECT pid
|
||||||
|
FROM pg_stat_activity
|
||||||
|
WHERE application_name = 'ayatori-database-management'
|
||||||
|
ORDER BY pid
|
||||||
|
`)
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
CREATE SCHEMA postgresql_tenant_operator;
|
||||||
|
|
||||||
|
CREATE TABLE postgresql_tenant_operator.tenant_ownership (
|
||||||
|
instance_uid text NOT NULL,
|
||||||
|
tenant_uid text NOT NULL,
|
||||||
|
tenant_namespace text NOT NULL,
|
||||||
|
tenant_name text NOT NULL,
|
||||||
|
database_name text NOT NULL,
|
||||||
|
role_name text NOT NULL,
|
||||||
|
credential_path text NOT NULL,
|
||||||
|
managed boolean NOT NULL DEFAULT true,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||||
|
retained_at timestamptz,
|
||||||
|
PRIMARY KEY (instance_uid, tenant_uid),
|
||||||
|
UNIQUE (instance_uid, tenant_namespace, tenant_name),
|
||||||
|
UNIQUE (instance_uid, database_name),
|
||||||
|
UNIQUE (instance_uid, role_name),
|
||||||
|
UNIQUE (credential_path),
|
||||||
|
CHECK (managed OR retained_at IS NOT NULL)
|
||||||
|
);
|
||||||
|
|
||||||
|
---- create above / drop below ----
|
||||||
|
|
||||||
|
DROP SCHEMA postgresql_tenant_operator CASCADE;
|
||||||
@@ -0,0 +1,326 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Package registry persists controller ownership evidence in the PostgreSQL
|
||||||
|
// management database. It deliberately does not persist reconciliation phases;
|
||||||
|
// those belong to the Kubernetes resource status.
|
||||||
|
package registry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"embed"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
"github.com/jackc/tern/v2/migrate"
|
||||||
|
)
|
||||||
|
|
||||||
|
const migrationVersionTable = "public.postgresql_tenant_operator_schema_version"
|
||||||
|
|
||||||
|
//go:embed migrations/*.sql
|
||||||
|
var migrationFiles embed.FS
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrNotFound indicates that the registry has no matching ownership record.
|
||||||
|
ErrNotFound = errors.New("registry ownership record not found")
|
||||||
|
// ErrConflict indicates that a requested name or path belongs to another Tenant UID.
|
||||||
|
ErrConflict = errors.New("registry ownership conflict")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Beginner is implemented by pgx.Conn and pgxpool.Pool.
|
||||||
|
type Beginner interface {
|
||||||
|
Begin(context.Context) (pgx.Tx, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store manages ownership records in one PostgreSQLInstance management database.
|
||||||
|
type Store struct {
|
||||||
|
db Beginner
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStore creates a registry store backed by a PostgreSQL connection or pool.
|
||||||
|
func NewStore(db Beginner) *Store {
|
||||||
|
return &Store{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ownership identifies every external resource reserved for one Tenant UID.
|
||||||
|
type Ownership struct {
|
||||||
|
InstanceUID string
|
||||||
|
TenantUID string
|
||||||
|
TenantNamespace string
|
||||||
|
TenantName string
|
||||||
|
DatabaseName string
|
||||||
|
RoleName string
|
||||||
|
CredentialPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record is the persisted ownership state.
|
||||||
|
type Record struct {
|
||||||
|
Ownership
|
||||||
|
Managed bool
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
RetainedAt *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClaimResult reports whether Claim inserted a new record or found the same claim.
|
||||||
|
type ClaimResult string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ClaimCreated ClaimResult = "Created"
|
||||||
|
ClaimOwned ClaimResult = "Owned"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bootstrap applies all pending versioned registry migrations idempotently.
|
||||||
|
func (s *Store) Bootstrap(ctx context.Context) error {
|
||||||
|
if s == nil || s.db == nil {
|
||||||
|
return errors.New("bootstrap registry: nil database")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.withMigrationConnection(ctx, func(conn *pgx.Conn) error {
|
||||||
|
migrations, err := fs.Sub(migrationFiles, "migrations")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open embedded registry migrations: %w", err)
|
||||||
|
}
|
||||||
|
migrator, err := migrate.NewMigrator(ctx, conn, migrationVersionTable)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("initialize registry migrator: %w", err)
|
||||||
|
}
|
||||||
|
if err := migrator.LoadMigrations(migrations); err != nil {
|
||||||
|
return fmt.Errorf("load registry migrations: %w", err)
|
||||||
|
}
|
||||||
|
if err := migrator.Migrate(ctx); err != nil {
|
||||||
|
return fmt.Errorf("apply registry migrations: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Claim reserves all names for an owner. Repeating an identical claim is idempotent.
|
||||||
|
func (s *Store) Claim(ctx context.Context, owner Ownership) (ClaimResult, error) {
|
||||||
|
if s == nil || s.db == nil {
|
||||||
|
return "", errors.New("claim registry ownership: nil database")
|
||||||
|
}
|
||||||
|
if err := owner.validate(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := s.db.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("begin registry claim: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback(ctx) }()
|
||||||
|
|
||||||
|
_, err = scanRecord(tx.QueryRow(ctx, claimStatement,
|
||||||
|
owner.InstanceUID,
|
||||||
|
owner.TenantUID,
|
||||||
|
owner.TenantNamespace,
|
||||||
|
owner.TenantName,
|
||||||
|
owner.DatabaseName,
|
||||||
|
owner.RoleName,
|
||||||
|
owner.CredentialPath,
|
||||||
|
))
|
||||||
|
if err != nil && !errors.Is(err, ErrNotFound) {
|
||||||
|
return "", fmt.Errorf("insert registry claim: %w", err)
|
||||||
|
}
|
||||||
|
if errors.Is(err, ErrNotFound) {
|
||||||
|
record, err := getByTenantUID(ctx, tx, owner.InstanceUID, owner.TenantUID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrNotFound) {
|
||||||
|
return "", fmt.Errorf("%w: database, role, tenant identity, or credential path is already reserved", ErrConflict)
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if !record.equal(owner) || !record.Managed {
|
||||||
|
return "", fmt.Errorf("%w: database, role, tenant identity, or credential path is already reserved", ErrConflict)
|
||||||
|
}
|
||||||
|
if err := tx.Commit(ctx); err != nil {
|
||||||
|
return "", fmt.Errorf("commit registry claim: %w", err)
|
||||||
|
}
|
||||||
|
return ClaimOwned, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(ctx); err != nil {
|
||||||
|
return "", fmt.Errorf("commit registry claim: %w", err)
|
||||||
|
}
|
||||||
|
return ClaimCreated, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) withMigrationConnection(ctx context.Context, run func(*pgx.Conn) error) error {
|
||||||
|
switch db := s.db.(type) {
|
||||||
|
case *pgx.Conn:
|
||||||
|
return run(db)
|
||||||
|
case *pgxpool.Pool:
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("acquire registry migration connection: %w", err)
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
return run(conn.Conn())
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("bootstrap registry: database type %T cannot provide a migration connection", s.db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns the ownership record for an Instance UID and Tenant UID.
|
||||||
|
func (s *Store) Get(ctx context.Context, instanceUID, tenantUID string) (Record, error) {
|
||||||
|
if s == nil || s.db == nil {
|
||||||
|
return Record{}, errors.New("get registry record: nil database")
|
||||||
|
}
|
||||||
|
if instanceUID == "" || tenantUID == "" {
|
||||||
|
return Record{}, errors.New("get registry record: instance UID and tenant UID are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := s.db.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return Record{}, fmt.Errorf("begin registry read: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback(ctx) }()
|
||||||
|
|
||||||
|
record, err := getByTenantUID(ctx, tx, instanceUID, tenantUID)
|
||||||
|
if err != nil {
|
||||||
|
return Record{}, err
|
||||||
|
}
|
||||||
|
if err := tx.Commit(ctx); err != nil {
|
||||||
|
return Record{}, fmt.Errorf("commit registry read: %w", err)
|
||||||
|
}
|
||||||
|
return record, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkRetained changes a matching managed ownership record into an unmanaged tombstone.
|
||||||
|
// Repeating the operation for the same tombstone is safe.
|
||||||
|
func (s *Store) MarkRetained(ctx context.Context, owner Ownership) error {
|
||||||
|
return s.changeOwnership(ctx, owner, "mark registry record retained", func(ctx context.Context, tx pgx.Tx, record Record) error {
|
||||||
|
if !record.Managed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tag, err := tx.Exec(ctx, markRetainedStatement, owner.InstanceUID, owner.TenantUID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() != 1 {
|
||||||
|
return ErrConflict
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes a matching managed record after its external resources have been deleted.
|
||||||
|
// An already absent record is treated as a successful retry; a retained record is never deleted.
|
||||||
|
func (s *Store) Delete(ctx context.Context, owner Ownership) error {
|
||||||
|
return s.changeOwnership(ctx, owner, "delete registry record", func(ctx context.Context, tx pgx.Tx, record Record) error {
|
||||||
|
if !record.Managed {
|
||||||
|
return ErrConflict
|
||||||
|
}
|
||||||
|
tag, err := tx.Exec(ctx, deleteStatement, owner.InstanceUID, owner.TenantUID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() != 1 {
|
||||||
|
return ErrConflict
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) changeOwnership(
|
||||||
|
ctx context.Context,
|
||||||
|
owner Ownership,
|
||||||
|
operation string,
|
||||||
|
change func(context.Context, pgx.Tx, Record) error,
|
||||||
|
) error {
|
||||||
|
if s == nil || s.db == nil {
|
||||||
|
return fmt.Errorf("%s: nil database", operation)
|
||||||
|
}
|
||||||
|
if err := owner.validate(); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", operation, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := s.db.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("begin %s: %w", operation, err)
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback(ctx) }()
|
||||||
|
|
||||||
|
record, err := getByTenantUIDForUpdate(ctx, tx, owner.InstanceUID, owner.TenantUID)
|
||||||
|
if errors.Is(err, ErrNotFound) && operation == "delete registry record" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", operation, err)
|
||||||
|
}
|
||||||
|
if !record.equal(owner) {
|
||||||
|
return fmt.Errorf("%s: %w", operation, ErrConflict)
|
||||||
|
}
|
||||||
|
if err := change(ctx, tx, record); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", operation, err)
|
||||||
|
}
|
||||||
|
if err := tx.Commit(ctx); err != nil {
|
||||||
|
return fmt.Errorf("commit %s: %w", operation, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getByTenantUIDForUpdate(ctx context.Context, tx pgx.Tx, instanceUID, tenantUID string) (Record, error) {
|
||||||
|
return scanRecord(tx.QueryRow(ctx, getByTenantUIDForUpdateStatement, instanceUID, tenantUID))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getByTenantUID(ctx context.Context, tx pgx.Tx, instanceUID, tenantUID string) (Record, error) {
|
||||||
|
return scanRecord(tx.QueryRow(ctx, getByTenantUIDStatement, instanceUID, tenantUID))
|
||||||
|
}
|
||||||
|
|
||||||
|
type rowScanner interface {
|
||||||
|
Scan(...any) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanRecord(row rowScanner) (Record, error) {
|
||||||
|
var record Record
|
||||||
|
err := row.Scan(
|
||||||
|
&record.InstanceUID,
|
||||||
|
&record.TenantUID,
|
||||||
|
&record.TenantNamespace,
|
||||||
|
&record.TenantName,
|
||||||
|
&record.DatabaseName,
|
||||||
|
&record.RoleName,
|
||||||
|
&record.CredentialPath,
|
||||||
|
&record.Managed,
|
||||||
|
&record.CreatedAt,
|
||||||
|
&record.UpdatedAt,
|
||||||
|
&record.RetainedAt,
|
||||||
|
)
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return Record{}, ErrNotFound
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return Record{}, fmt.Errorf("read registry record: %w", err)
|
||||||
|
}
|
||||||
|
return record, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Ownership) validate() error {
|
||||||
|
if o.InstanceUID == "" || o.TenantUID == "" || o.TenantNamespace == "" || o.TenantName == "" ||
|
||||||
|
o.DatabaseName == "" || o.RoleName == "" || o.CredentialPath == "" {
|
||||||
|
return errors.New("claim registry ownership: all ownership fields are required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Ownership) equal(other Ownership) bool {
|
||||||
|
return o == other
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package registry
|
||||||
|
|
||||||
|
const claimStatement = `
|
||||||
|
INSERT INTO postgresql_tenant_operator.tenant_ownership (
|
||||||
|
instance_uid,
|
||||||
|
tenant_uid,
|
||||||
|
tenant_namespace,
|
||||||
|
tenant_name,
|
||||||
|
database_name,
|
||||||
|
role_name,
|
||||||
|
credential_path
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
RETURNING
|
||||||
|
instance_uid,
|
||||||
|
tenant_uid,
|
||||||
|
tenant_namespace,
|
||||||
|
tenant_name,
|
||||||
|
database_name,
|
||||||
|
role_name,
|
||||||
|
credential_path,
|
||||||
|
managed,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
retained_at`
|
||||||
|
|
||||||
|
const getByTenantUIDStatement = `
|
||||||
|
SELECT
|
||||||
|
instance_uid,
|
||||||
|
tenant_uid,
|
||||||
|
tenant_namespace,
|
||||||
|
tenant_name,
|
||||||
|
database_name,
|
||||||
|
role_name,
|
||||||
|
credential_path,
|
||||||
|
managed,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
retained_at
|
||||||
|
FROM postgresql_tenant_operator.tenant_ownership
|
||||||
|
WHERE instance_uid = $1 AND tenant_uid = $2`
|
||||||
|
|
||||||
|
const getByTenantUIDForUpdateStatement = getByTenantUIDStatement + ` FOR UPDATE`
|
||||||
|
|
||||||
|
const markRetainedStatement = `
|
||||||
|
UPDATE postgresql_tenant_operator.tenant_ownership
|
||||||
|
SET managed = false, retained_at = clock_timestamp(), updated_at = clock_timestamp()
|
||||||
|
WHERE instance_uid = $1 AND tenant_uid = $2 AND managed = true`
|
||||||
|
|
||||||
|
const deleteStatement = `
|
||||||
|
DELETE FROM postgresql_tenant_operator.tenant_ownership
|
||||||
|
WHERE instance_uid = $1 AND tenant_uid = $2 AND managed = true`
|
||||||
@@ -0,0 +1,427 @@
|
|||||||
|
//go:build integration
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2026.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package postgresql_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
|
||||||
|
"git.ddupan.top/panxiao81/ayatori/internal/database/adapter/postgresql/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registryFixture 只连接本测试创建的容器,不接受外部数据库地址。
|
||||||
|
type registryFixture struct {
|
||||||
|
ctx context.Context
|
||||||
|
pool *pgxpool.Pool
|
||||||
|
store *registry.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRegistryFixture(t *testing.T) *registryFixture {
|
||||||
|
t.Helper()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
_, port := postgresFixture(t, ctx)
|
||||||
|
endpoint := url.URL{
|
||||||
|
Scheme: "postgres",
|
||||||
|
User: url.UserPassword(fixtureUser, fixturePassword),
|
||||||
|
Host: net.JoinHostPort("127.0.0.1", strconv.Itoa(port)),
|
||||||
|
Path: "/postgres",
|
||||||
|
RawQuery: "sslmode=disable",
|
||||||
|
}
|
||||||
|
pool, err := pgxpool.New(ctx, endpoint.String())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("cannot configure registry fixture connection")
|
||||||
|
}
|
||||||
|
t.Cleanup(pool.Close)
|
||||||
|
if err := pool.Ping(ctx); err != nil {
|
||||||
|
t.Fatal("cannot connect to registry fixture")
|
||||||
|
}
|
||||||
|
return ®istryFixture{ctx: ctx, pool: pool, store: registry.NewStore(pool)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func registryOwner(suffix string) registry.Ownership {
|
||||||
|
return registry.Ownership{
|
||||||
|
InstanceUID: "instance-uid",
|
||||||
|
TenantUID: "tenant-uid-" + suffix,
|
||||||
|
TenantNamespace: "applications",
|
||||||
|
TenantName: "tenant-" + suffix,
|
||||||
|
DatabaseName: "database_" + suffix,
|
||||||
|
RoleName: "role_" + suffix,
|
||||||
|
CredentialPath: "credentials/" + suffix,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *registryFixture) bootstrap(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
if err := f.store.Bootstrap(f.ctx); err != nil {
|
||||||
|
t.Fatalf("bootstrap registry: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *registryFixture) claim(t *testing.T, owner registry.Ownership, expected registry.ClaimResult) {
|
||||||
|
t.Helper()
|
||||||
|
result, err := f.store.Claim(f.ctx, owner)
|
||||||
|
if err != nil || result != expected {
|
||||||
|
t.Fatalf("claim: result=%q, error=%v; want %q", result, err, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegistryOwnershipLifecycle(t *testing.T) {
|
||||||
|
f := newRegistryFixture(t)
|
||||||
|
f.bootstrap(t)
|
||||||
|
f.bootstrap(t)
|
||||||
|
owner := registryOwner("lifecycle")
|
||||||
|
f.claim(t, owner, registry.ClaimCreated)
|
||||||
|
f.claim(t, owner, registry.ClaimOwned)
|
||||||
|
record, err := f.store.Get(f.ctx, owner.InstanceUID, owner.TenantUID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if record.Ownership != owner || !record.Managed || record.CreatedAt.IsZero() || record.RetainedAt != nil {
|
||||||
|
t.Fatalf("unexpected ownership record: %+v", record)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误的资源归属不能删除或 Retain 原记录。
|
||||||
|
wrongOwner := owner
|
||||||
|
wrongOwner.RoleName = "another_role"
|
||||||
|
if err := f.store.Delete(f.ctx, wrongOwner); !errors.Is(err, registry.ErrConflict) {
|
||||||
|
t.Fatalf("delete mismatched owner: %v", err)
|
||||||
|
}
|
||||||
|
if err := f.store.MarkRetained(f.ctx, wrongOwner); !errors.Is(err, registry.ErrConflict) {
|
||||||
|
t.Fatalf("retain mismatched owner: %v", err)
|
||||||
|
}
|
||||||
|
for range 2 {
|
||||||
|
if err := f.store.Delete(f.ctx, owner); err != nil {
|
||||||
|
t.Fatalf("delete retry: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := f.store.Get(f.ctx, owner.InstanceUID, owner.TenantUID); !errors.Is(err, registry.ErrNotFound) {
|
||||||
|
t.Fatalf("read deleted record: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegistryRetainedRecordCannotBeReclaimed(t *testing.T) {
|
||||||
|
f := newRegistryFixture(t)
|
||||||
|
f.bootstrap(t)
|
||||||
|
owner := registryOwner("retained")
|
||||||
|
f.claim(t, owner, registry.ClaimCreated)
|
||||||
|
if err := f.store.MarkRetained(f.ctx, owner); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
retained, err := f.store.Get(f.ctx, owner.InstanceUID, owner.TenantUID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if retained.Managed || retained.RetainedAt == nil {
|
||||||
|
t.Fatalf("missing retained tombstone: %+v", retained)
|
||||||
|
}
|
||||||
|
if err := f.store.MarkRetained(f.ctx, owner); err != nil {
|
||||||
|
t.Fatalf("retain retry: %v", err)
|
||||||
|
}
|
||||||
|
retried, err := f.store.Get(f.ctx, owner.InstanceUID, owner.TenantUID)
|
||||||
|
if err != nil || !retried.UpdatedAt.Equal(retained.UpdatedAt) {
|
||||||
|
t.Fatalf("retain retry changed tombstone: %+v, %v", retried, err)
|
||||||
|
}
|
||||||
|
if err := f.store.Delete(f.ctx, owner); !errors.Is(err, registry.ErrConflict) {
|
||||||
|
t.Fatalf("delete retained record: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := f.store.Claim(f.ctx, owner); !errors.Is(err, registry.ErrConflict) {
|
||||||
|
t.Fatalf("reclaim retained record: %v", err)
|
||||||
|
}
|
||||||
|
owner.TenantUID = "replacement-uid"
|
||||||
|
if _, err := f.store.Claim(f.ctx, owner); !errors.Is(err, registry.ErrConflict) {
|
||||||
|
t.Fatalf("replacement tenant reclaimed tombstone: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegistryUniqueReservations(t *testing.T) {
|
||||||
|
f := newRegistryFixture(t)
|
||||||
|
f.bootstrap(t)
|
||||||
|
owner := registryOwner("original")
|
||||||
|
f.claim(t, owner, registry.ClaimCreated)
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
change func(*registry.Ownership)
|
||||||
|
}{
|
||||||
|
{name: "tenant UID", change: func(other *registry.Ownership) { other.TenantUID = owner.TenantUID }},
|
||||||
|
{name: "tenant name", change: func(other *registry.Ownership) { other.TenantName = owner.TenantName }},
|
||||||
|
{name: "database", change: func(other *registry.Ownership) { other.DatabaseName = owner.DatabaseName }},
|
||||||
|
{name: "role", change: func(other *registry.Ownership) { other.RoleName = owner.RoleName }},
|
||||||
|
{name: "credential path", change: func(other *registry.Ownership) { other.CredentialPath = owner.CredentialPath }},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
other := registryOwner("other")
|
||||||
|
tt.change(&other)
|
||||||
|
if _, err := f.store.Claim(f.ctx, other); !errors.Is(err, registry.ErrConflict) {
|
||||||
|
t.Fatalf("conflicting reservation: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegistryConcurrentBootstrapAndClaims(t *testing.T) {
|
||||||
|
f := newRegistryFixture(t)
|
||||||
|
const workers = 8
|
||||||
|
start := make(chan struct{})
|
||||||
|
results := make(chan error, workers)
|
||||||
|
var group sync.WaitGroup
|
||||||
|
for range workers {
|
||||||
|
group.Go(func() {
|
||||||
|
<-start
|
||||||
|
results <- f.store.Bootstrap(f.ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
close(start)
|
||||||
|
group.Wait()
|
||||||
|
close(results)
|
||||||
|
for err := range results {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("concurrent bootstrap: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
owner := registryOwner("concurrent")
|
||||||
|
type claimOutcome struct {
|
||||||
|
result registry.ClaimResult
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
claims := make(chan claimOutcome, workers)
|
||||||
|
start = make(chan struct{})
|
||||||
|
for range workers {
|
||||||
|
group.Go(func() {
|
||||||
|
<-start
|
||||||
|
result, err := f.store.Claim(f.ctx, owner)
|
||||||
|
claims <- claimOutcome{result: result, err: err}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
close(start)
|
||||||
|
group.Wait()
|
||||||
|
close(claims)
|
||||||
|
created := 0
|
||||||
|
for outcome := range claims {
|
||||||
|
if outcome.err != nil {
|
||||||
|
t.Fatal(outcome.err)
|
||||||
|
}
|
||||||
|
switch outcome.result {
|
||||||
|
case registry.ClaimCreated:
|
||||||
|
created++
|
||||||
|
case registry.ClaimOwned:
|
||||||
|
default:
|
||||||
|
t.Fatalf("unexpected claim result: %q", outcome.result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if created != 1 {
|
||||||
|
t.Fatalf("created %d records for the same owner; want 1", created)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegistryRestartAndCanceledRequestRecovery(t *testing.T) {
|
||||||
|
f := newRegistryFixture(t)
|
||||||
|
f.bootstrap(t)
|
||||||
|
owner := registryOwner("restart")
|
||||||
|
f.claim(t, owner, registry.ClaimCreated)
|
||||||
|
config := f.pool.Config()
|
||||||
|
f.pool.Close()
|
||||||
|
pool, err := pgxpool.NewWithConfig(f.ctx, config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("cannot reopen fixture connection")
|
||||||
|
}
|
||||||
|
t.Cleanup(pool.Close)
|
||||||
|
f.store = registry.NewStore(pool)
|
||||||
|
f.bootstrap(t)
|
||||||
|
// 模拟客户端丢失上次成功结果后,以新连接重试;归属证据必须来自数据库。
|
||||||
|
f.claim(t, owner, registry.ClaimOwned)
|
||||||
|
canceled, cancel := context.WithCancel(f.ctx)
|
||||||
|
cancel()
|
||||||
|
if _, err := f.store.Claim(canceled, registryOwner("canceled")); !errors.Is(err, context.Canceled) {
|
||||||
|
t.Fatalf("canceled claim: %v", err)
|
||||||
|
}
|
||||||
|
f.claim(t, registryOwner("canceled"), registry.ClaimCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegistryBootstrapRejectsUnknownSchema(t *testing.T) {
|
||||||
|
f := newRegistryFixture(t)
|
||||||
|
// 人工创建的同名 schema 不能被初始化过程接管或覆盖。
|
||||||
|
if _, err := f.pool.Exec(f.ctx, "CREATE SCHEMA postgresql_tenant_operator"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := f.store.Bootstrap(f.ctx); err == nil {
|
||||||
|
t.Fatal("bootstrap accepted an unmanaged schema")
|
||||||
|
}
|
||||||
|
var version int
|
||||||
|
if err := f.pool.QueryRow(f.ctx, "SELECT version FROM public.postgresql_tenant_operator_schema_version").Scan(&version); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if version != 0 {
|
||||||
|
t.Fatalf("failed migration advanced version to %d", version)
|
||||||
|
}
|
||||||
|
// 仅在本测试拥有的临时数据库中清除空冲突 schema,然后重试失败的迁移。
|
||||||
|
if _, err := f.pool.Exec(f.ctx, "DROP SCHEMA postgresql_tenant_operator"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f.bootstrap(t)
|
||||||
|
if _, err := f.pool.Exec(f.ctx, "UPDATE public.postgresql_tenant_operator_schema_version SET version = 100"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := f.store.Bootstrap(f.ctx); err == nil {
|
||||||
|
t.Fatal("bootstrap accepted a future migration version")
|
||||||
|
}
|
||||||
|
if err := f.pool.QueryRow(f.ctx, "SELECT version FROM public.postgresql_tenant_operator_schema_version").Scan(&version); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if version != 100 {
|
||||||
|
t.Fatalf("bootstrap rewrote future version to %d", version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数据库执行真实 COMMIT 后才注入客户端错误,模拟客户端无法确认提交结果。
|
||||||
|
// 这不是网络故障测试,但能确定性覆盖已提交、调用者却收到失败的恢复分支。
|
||||||
|
type lostCommitReply struct {
|
||||||
|
registry.Beginner
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db lostCommitReply) Begin(ctx context.Context) (pgx.Tx, error) {
|
||||||
|
tx, err := db.Beginner.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return uncertainCommit{Tx: tx, err: db.err}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type uncertainCommit struct {
|
||||||
|
pgx.Tx
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tx uncertainCommit) Commit(ctx context.Context) error {
|
||||||
|
if err := tx.Tx.Commit(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegistryRetriesAfterLostCommitReply(t *testing.T) {
|
||||||
|
f := newRegistryFixture(t)
|
||||||
|
f.bootstrap(t)
|
||||||
|
replyLost := errors.New("test-only lost commit reply")
|
||||||
|
uncertain := registry.NewStore(lostCommitReply{Beginner: f.pool, err: replyLost})
|
||||||
|
owner := registryOwner("uncertain")
|
||||||
|
if _, err := uncertain.Claim(f.ctx, owner); !errors.Is(err, replyLost) {
|
||||||
|
t.Fatalf("claim did not report lost reply: %v", err)
|
||||||
|
}
|
||||||
|
f.claim(t, owner, registry.ClaimOwned)
|
||||||
|
if err := uncertain.MarkRetained(f.ctx, owner); !errors.Is(err, replyLost) {
|
||||||
|
t.Fatalf("retain did not report lost reply: %v", err)
|
||||||
|
}
|
||||||
|
if err := f.store.MarkRetained(f.ctx, owner); err != nil {
|
||||||
|
t.Fatalf("retry uncertain retain: %v", err)
|
||||||
|
}
|
||||||
|
if err := f.store.Delete(f.ctx, owner); !errors.Is(err, registry.ErrConflict) {
|
||||||
|
t.Fatalf("uncertain retain lost tombstone protection: %v", err)
|
||||||
|
}
|
||||||
|
deletable := registryOwner("uncertain_delete")
|
||||||
|
f.claim(t, deletable, registry.ClaimCreated)
|
||||||
|
if err := uncertain.Delete(f.ctx, deletable); !errors.Is(err, replyLost) {
|
||||||
|
t.Fatalf("delete did not report lost reply: %v", err)
|
||||||
|
}
|
||||||
|
if err := f.store.Delete(f.ctx, deletable); err != nil {
|
||||||
|
t.Fatalf("retry uncertain delete: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegistryConcurrentConflictingClaims(t *testing.T) {
|
||||||
|
f := newRegistryFixture(t)
|
||||||
|
f.bootstrap(t)
|
||||||
|
first := registryOwner("first")
|
||||||
|
second := registryOwner("second")
|
||||||
|
second.DatabaseName = first.DatabaseName
|
||||||
|
start := make(chan struct{})
|
||||||
|
results := make(chan error, 2)
|
||||||
|
var group sync.WaitGroup
|
||||||
|
for _, owner := range []registry.Ownership{first, second} {
|
||||||
|
group.Go(func() {
|
||||||
|
<-start
|
||||||
|
_, err := f.store.Claim(f.ctx, owner)
|
||||||
|
results <- err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
close(start)
|
||||||
|
group.Wait()
|
||||||
|
close(results)
|
||||||
|
created, conflicts := 0, 0
|
||||||
|
for err := range results {
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
created++
|
||||||
|
case errors.Is(err, registry.ErrConflict):
|
||||||
|
conflicts++
|
||||||
|
default:
|
||||||
|
t.Fatalf("unexpected concurrent claim error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if created != 1 || conflicts != 1 {
|
||||||
|
t.Fatalf("concurrent reservation: created=%d conflicts=%d; want one of each", created, conflicts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegistryConcurrentRetainAndDelete(t *testing.T) {
|
||||||
|
f := newRegistryFixture(t)
|
||||||
|
f.bootstrap(t)
|
||||||
|
owner := registryOwner("delete_race")
|
||||||
|
f.claim(t, owner, registry.ClaimCreated)
|
||||||
|
start := make(chan struct{})
|
||||||
|
retainResult := make(chan error, 1)
|
||||||
|
deleteResult := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
<-start
|
||||||
|
retainResult <- f.store.MarkRetained(f.ctx, owner)
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
<-start
|
||||||
|
deleteResult <- f.store.Delete(f.ctx, owner)
|
||||||
|
}()
|
||||||
|
close(start)
|
||||||
|
retainErr, deleteErr := <-retainResult, <-deleteResult
|
||||||
|
record, readErr := f.store.Get(f.ctx, owner.InstanceUID, owner.TenantUID)
|
||||||
|
switch {
|
||||||
|
case retainErr == nil:
|
||||||
|
// Retain 先取得行锁时,Delete 必须拒绝删除墓碑。
|
||||||
|
if !errors.Is(deleteErr, registry.ErrConflict) || readErr != nil || record.Managed || record.RetainedAt == nil {
|
||||||
|
t.Fatalf("retain won but tombstone was not protected: delete=%v read=%v record=%+v", deleteErr, readErr, record)
|
||||||
|
}
|
||||||
|
case errors.Is(retainErr, registry.ErrNotFound):
|
||||||
|
// Delete 先提交时,Retain 必须报告记录已不存在,不能重建墓碑。
|
||||||
|
if deleteErr != nil || !errors.Is(readErr, registry.ErrNotFound) {
|
||||||
|
t.Fatalf("delete won but record remains: delete=%v read=%v", deleteErr, readErr)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatalf("unexpected retain/delete race: retain=%v delete=%v", retainErr, deleteErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
//go:build integration
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2026.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package postgresql_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/pem"
|
||||||
|
"math/big"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.ddupan.top/panxiao81/ayatori/internal/database/adapter/postgresql"
|
||||||
|
"git.ddupan.top/panxiao81/ayatori/internal/database/application"
|
||||||
|
"git.ddupan.top/panxiao81/ayatori/internal/database/domain/instance"
|
||||||
|
)
|
||||||
|
|
||||||
|
func fixtureCertificate(t *testing.T) (string, string) {
|
||||||
|
t.Helper()
|
||||||
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
cert := &x509.Certificate{
|
||||||
|
SerialNumber: big.NewInt(1),
|
||||||
|
Subject: pkix.Name{CommonName: fixtureHost},
|
||||||
|
NotBefore: time.Now().Add(-time.Hour),
|
||||||
|
NotAfter: time.Now().Add(time.Hour),
|
||||||
|
DNSNames: []string{fixtureHost},
|
||||||
|
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
|
||||||
|
IsCA: true,
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||||
|
}
|
||||||
|
der, err := x509.CreateCertificate(rand.Reader, cert, cert, &key.PublicKey, key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
encodedKey, err := x509.MarshalECPrivateKey(key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
dir := t.TempDir()
|
||||||
|
certPath := filepath.Join(dir, "server.crt")
|
||||||
|
keyPath := filepath.Join(dir, "server.key")
|
||||||
|
if err := os.WriteFile(certPath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(keyPath, pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: encodedKey}), 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return certPath, keyPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostgreSQLTLSHostIdentity(t *testing.T) {
|
||||||
|
const psql = "psql"
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
id, port := postgresFixture(t, ctx)
|
||||||
|
certPath, keyPath := fixtureCertificate(t)
|
||||||
|
commands := [][]string{
|
||||||
|
{"cp", certPath, id + ":/tmp/server.crt"},
|
||||||
|
{"cp", keyPath, id + ":/tmp/server.key"},
|
||||||
|
{dockerExec, "-u", "0", id, "chown", "postgres:postgres", "/tmp/server.crt", "/tmp/server.key"},
|
||||||
|
{dockerExec, id, psql, "-U", fixtureUser, "-c", "ALTER SYSTEM SET ssl_cert_file='/tmp/server.crt'"},
|
||||||
|
{dockerExec, id, psql, "-U", fixtureUser, "-c", "ALTER SYSTEM SET ssl_key_file='/tmp/server.key'"},
|
||||||
|
{dockerExec, id, psql, "-U", fixtureUser, "-c", "ALTER SYSTEM SET ssl=on"},
|
||||||
|
{dockerExec, id, psql, "-U", fixtureUser, "-c", "SELECT pg_reload_conf()"},
|
||||||
|
}
|
||||||
|
for _, args := range commands {
|
||||||
|
if exec.CommandContext(ctx, "docker", args...).Run() != nil {
|
||||||
|
t.Fatal("TLS fixture setup failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
credentials, err := application.NewCredentials(fixtureUser, fixturePassword)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
connector := postgresql.Connector{RootCert: certPath}
|
||||||
|
endpoint := target(t, port, instance.TLSVerifyFull).Definition().Endpoint()
|
||||||
|
db, err := connector.Connect(ctx, endpoint, credentials)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("trusted DNS SAN connection failed", err)
|
||||||
|
}
|
||||||
|
if version, err := db.Version(ctx); err != nil || version == "" {
|
||||||
|
db.Close()
|
||||||
|
t.Fatal("TLS metadata read failed", err)
|
||||||
|
}
|
||||||
|
db.Close()
|
||||||
|
values := endpoint.Values()
|
||||||
|
values.Host = "127.0.0.1"
|
||||||
|
ipEndpoint, err := instance.NewEndpoint(values)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
db, err = connector.Connect(ctx, ipEndpoint, credentials)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("trusted IP SAN connection failed", err)
|
||||||
|
}
|
||||||
|
db.Close()
|
||||||
|
values.Host = "wrong.invalid"
|
||||||
|
wrongEndpoint, err := instance.NewEndpoint(values)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if db, err := connector.Connect(ctx, wrongEndpoint, credentials); err == nil {
|
||||||
|
db.Close()
|
||||||
|
t.Fatal("wrong TLS hostname accepted")
|
||||||
|
}
|
||||||
|
otherCA, _ := fixtureCertificate(t)
|
||||||
|
if db, err := (postgresql.Connector{RootCert: otherCA}).Connect(ctx, endpoint, credentials); err == nil {
|
||||||
|
db.Close()
|
||||||
|
t.Fatal("wrong CA accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Package application 定义 Database 用例与适配器之间的边界。
|
||||||
|
package application
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"git.ddupan.top/panxiao81/ayatori/internal/database/domain/instance"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrCredentialsUnavailable = errors.New("management credentials unavailable")
|
||||||
|
ErrCredentialsInvalid = errors.New("management credentials invalid")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Credentials 只存在于应用与连接适配器内存,不进入领域对象或持久化状态。
|
||||||
|
type Credentials struct {
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCredentials(username, password string) (Credentials, error) {
|
||||||
|
if username == "" || password == "" {
|
||||||
|
return Credentials{}, ErrCredentialsInvalid
|
||||||
|
}
|
||||||
|
return Credentials{username: username, password: password}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Credentials) Username() string { return c.username }
|
||||||
|
func (c Credentials) Password() string { return c.password }
|
||||||
|
func (c Credentials) String() string { return "[redacted management credentials]" }
|
||||||
|
func (c Credentials) GoString() string { return c.String() }
|
||||||
|
|
||||||
|
// MarshalJSON 显式隐藏内容,避免未来字段调整意外改变日志或序列化行为。
|
||||||
|
func (c Credentials) MarshalJSON() ([]byte, error) {
|
||||||
|
return []byte(`"[redacted management credentials]"`), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CredentialReader 返回本次读取的有效值;metadata 不参与凭据相等比较。
|
||||||
|
type CredentialReader interface {
|
||||||
|
Read(context.Context, instance.CredentialReference) (Credentials, error)
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package application
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const testUsername = "test-user"
|
||||||
|
|
||||||
|
func TestCredentialsRejectEmptyValues(t *testing.T) {
|
||||||
|
for _, values := range [][2]string{
|
||||||
|
{"", "test-password"},
|
||||||
|
{testUsername, ""},
|
||||||
|
{"", ""},
|
||||||
|
} {
|
||||||
|
if _, err := NewCredentials(values[0], values[1]); err != ErrCredentialsInvalid {
|
||||||
|
t.Fatal("empty credential was accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCredentialAndServiceFormattingIsRedacted(t *testing.T) {
|
||||||
|
const canary = "SECRET-CANARY-never-log-this"
|
||||||
|
credentials, err := NewCredentials(canary, canary)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if credentials.Username() != canary || credentials.Password() != canary {
|
||||||
|
t.Fatal("explicit credential access changed values")
|
||||||
|
}
|
||||||
|
|
||||||
|
service, err := NewInstanceService(&sourceStub{credentials: credentials}, &connectorStub{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(service.Close)
|
||||||
|
|
||||||
|
encoded, err := json.Marshal(credentials)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
outputs := []string{
|
||||||
|
string(encoded),
|
||||||
|
fmt.Sprintf("%v %+v %#v", credentials, credentials, credentials),
|
||||||
|
fmt.Sprintf("%v %+v %#v", service, service, service),
|
||||||
|
}
|
||||||
|
for _, output := range outputs {
|
||||||
|
if strings.Contains(output, canary) {
|
||||||
|
t.Fatal("formatting leaked credential data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package application
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"git.ddupan.top/panxiao81/ayatori/internal/database/domain/instance"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrConnection = errors.New("management connection unavailable")
|
||||||
|
ErrAuthentication = errors.New("management authentication failed")
|
||||||
|
ErrObservation = errors.New("management observation failed")
|
||||||
|
ErrCredentialsChanged = errors.New("management credentials changed during observation")
|
||||||
|
ErrClosed = errors.New("instance service closed")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Database 与 Connector 复用原项目 internal/instance/service.go 的能力边界。
|
||||||
|
// 版本查询只是本切片的连通性观察,不能产生领域 Ready。
|
||||||
|
type Database interface {
|
||||||
|
Version(context.Context) (string, error)
|
||||||
|
Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Connector interface {
|
||||||
|
Connect(context.Context, instance.Endpoint, Credentials) (Database, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type entry struct {
|
||||||
|
target instance.ObservationTarget
|
||||||
|
credentials Credentials
|
||||||
|
database Database
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstanceService 由原 Service 迁移:连接复用与释放属于应用装配,不属于 SQL adapter。
|
||||||
|
// 保留原实现串行操作的约束,防止 Close 与查询并发;controller 停止 worker 后调用 Close。
|
||||||
|
// 不缓存能力观察,不把连接存活等同于 Ready。凭据每轮重新读取,而非只在引用变化时读取。
|
||||||
|
type InstanceService struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
source CredentialReader
|
||||||
|
connector Connector
|
||||||
|
entries map[string]*entry
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInstanceService(source CredentialReader, connector Connector) (*InstanceService, error) {
|
||||||
|
if source == nil || connector == nil {
|
||||||
|
return nil, errors.New("credential source and connector required")
|
||||||
|
}
|
||||||
|
return &InstanceService{
|
||||||
|
source: source,
|
||||||
|
connector: connector,
|
||||||
|
entries: make(map[string]*entry),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InstanceService) String() string { return "[redacted instance service]" }
|
||||||
|
func (s *InstanceService) GoString() string { return s.String() }
|
||||||
|
|
||||||
|
// ObserveVersion 返回当前目标和凭据下的版本;任何失败均返回空结果。
|
||||||
|
// 调用者仍需使用 CR resourceVersion 保存前提防止 spec 并发修改;本方法不建立跨系统事务。
|
||||||
|
func (s *InstanceService) ObserveVersion(ctx context.Context, target instance.ObservationTarget) (string, error) {
|
||||||
|
if err := target.Validate(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
if s.closed {
|
||||||
|
return "", ErrClosed
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先读取有效凭据。读取失败时不得继续使用缓存中的旧连接。
|
||||||
|
name := target.Identity().Name()
|
||||||
|
credentials, err := s.source.Read(ctx, target.Definition().AdminCredential())
|
||||||
|
if err != nil {
|
||||||
|
s.release(name)
|
||||||
|
return "", credentialError(err)
|
||||||
|
}
|
||||||
|
if credentials.username == "" || credentials.password == "" {
|
||||||
|
s.release(name)
|
||||||
|
return "", ErrCredentialsInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接身份与有效值均未变化时复用 pgxpool;generation 本身不要求换池。
|
||||||
|
current := s.entries[name]
|
||||||
|
if current != nil && (current.target.Identity() != target.Identity() ||
|
||||||
|
current.target.Definition() != target.Definition() || current.credentials != credentials) {
|
||||||
|
s.release(name)
|
||||||
|
current = nil
|
||||||
|
}
|
||||||
|
if current == nil {
|
||||||
|
database, err := s.connector.Connect(ctx, target.Definition().Endpoint(), credentials)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
current = &entry{
|
||||||
|
target: target,
|
||||||
|
credentials: credentials,
|
||||||
|
database: database,
|
||||||
|
}
|
||||||
|
s.entries[name] = current
|
||||||
|
}
|
||||||
|
|
||||||
|
version, err := current.database.Version(ctx)
|
||||||
|
if err != nil {
|
||||||
|
s.release(name)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回读后再检查凭据,避免把轮换前取得的结果交给新凭据的调用链。
|
||||||
|
latest, err := s.source.Read(ctx, target.Definition().AdminCredential())
|
||||||
|
if err != nil {
|
||||||
|
s.release(name)
|
||||||
|
return "", credentialError(err)
|
||||||
|
}
|
||||||
|
if latest != credentials {
|
||||||
|
s.release(name)
|
||||||
|
return "", ErrCredentialsChanged
|
||||||
|
}
|
||||||
|
return version, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func credentialError(err error) error {
|
||||||
|
if errors.Is(err, ErrCredentialsInvalid) {
|
||||||
|
return ErrCredentialsInvalid
|
||||||
|
}
|
||||||
|
return ErrCredentialsUnavailable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forget 只释放本地连接;不删除数据库或 registry,不替代 Instance finalizer。
|
||||||
|
func (s *InstanceService) Forget(name string) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
s.release(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InstanceService) release(name string) {
|
||||||
|
if current := s.entries[name]; current != nil {
|
||||||
|
current.database.Close()
|
||||||
|
}
|
||||||
|
delete(s.entries, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InstanceService) Close() {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
s.closed = true
|
||||||
|
for name := range s.entries {
|
||||||
|
s.release(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package application
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.ddupan.top/panxiao81/ayatori/internal/database/domain/instance"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 延续源项目 Service 测试,用于穷举身份与装配失败;真实行为由 adapter 集成测试验证。
|
||||||
|
type sourceStub struct {
|
||||||
|
credentials Credentials
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sourceStub) Read(context.Context, instance.CredentialReference) (Credentials, error) {
|
||||||
|
return s.credentials, s.err
|
||||||
|
}
|
||||||
|
|
||||||
|
type databaseStub struct {
|
||||||
|
closes int
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *databaseStub) Version(context.Context) (string, error) { return "17", d.err }
|
||||||
|
func (d *databaseStub) Close() {
|
||||||
|
d.closes++
|
||||||
|
}
|
||||||
|
|
||||||
|
type connectorStub struct {
|
||||||
|
databases []*databaseStub
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *connectorStub) Connect(context.Context, instance.Endpoint, Credentials) (Database, error) {
|
||||||
|
if c.err != nil {
|
||||||
|
return nil, c.err
|
||||||
|
}
|
||||||
|
db := &databaseStub{}
|
||||||
|
c.databases = append(c.databases, db)
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func serviceTarget(t *testing.T, uid, host, secret string, generation int64) instance.ObservationTarget {
|
||||||
|
t.Helper()
|
||||||
|
id, err := instance.NewIdentity(uid, "shared")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
revision, err := instance.NewRevision(generation)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
endpoint, err := instance.NewEndpoint(instance.EndpointValues{
|
||||||
|
Host: host,
|
||||||
|
HostAddr: "127.0.0.1",
|
||||||
|
Port: 5432,
|
||||||
|
ManagementDatabase: "postgres",
|
||||||
|
TLSMode: instance.TLSDisable,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ref, err := instance.NewCredentialReference(instance.CredentialReferenceValues{
|
||||||
|
Name: secret,
|
||||||
|
UsernameKey: "user",
|
||||||
|
PasswordKey: "pass",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
definition, err := instance.NewDefinition(endpoint, ref)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
target, err := instance.NewObservationTarget(id, revision, definition)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstanceConnectionIdentity(t *testing.T) {
|
||||||
|
source := &sourceStub{credentials: Credentials{username: testUsername, password: "test-only"}}
|
||||||
|
connector := &connectorStub{}
|
||||||
|
service, err := NewInstanceService(source, connector)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer service.Close()
|
||||||
|
ctx := context.Background()
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
target instance.ObservationTarget
|
||||||
|
wantConnections int
|
||||||
|
}{
|
||||||
|
{"initial connection", serviceTarget(t, "uid-1", "first", "admin", 1), 1},
|
||||||
|
{"generation alone", serviceTarget(t, "uid-1", "first", "admin", 2), 1},
|
||||||
|
{"endpoint changed", serviceTarget(t, "uid-1", "second", "admin", 3), 2},
|
||||||
|
{"reference changed", serviceTarget(t, "uid-1", "second", "replacement", 4), 3},
|
||||||
|
{"same name with new UID", serviceTarget(t, "uid-2", "second", "replacement", 1), 4},
|
||||||
|
}
|
||||||
|
for _, testCase := range cases {
|
||||||
|
if _, err := service.ObserveVersion(ctx, testCase.target); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(connector.databases) != testCase.wantConnections {
|
||||||
|
t.Fatalf("%s: got %d connections, want %d", testCase.name, len(connector.databases), testCase.wantConnections)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, db := range connector.databases[:3] {
|
||||||
|
if db.closes != 1 {
|
||||||
|
t.Fatal("replaced connection not closed exactly once")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
service.Forget("shared")
|
||||||
|
service.Forget("shared")
|
||||||
|
if connector.databases[3].closes != 1 {
|
||||||
|
t.Fatal("forget did not close exactly once")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstanceAssemblyFailureRecovery(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
target := serviceTarget(t, "uid-1", "first", "admin", 1)
|
||||||
|
source := &sourceStub{
|
||||||
|
credentials: Credentials{username: testUsername, password: "test-only"},
|
||||||
|
err: errors.New("unsafe source error"),
|
||||||
|
}
|
||||||
|
connector := &connectorStub{err: ErrConnection}
|
||||||
|
service, err := NewInstanceService(source, connector)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer service.Close()
|
||||||
|
if version, err := service.ObserveVersion(ctx, target); version != "" || !errors.Is(err, ErrCredentialsUnavailable) {
|
||||||
|
t.Fatal("unsafe source error escaped")
|
||||||
|
}
|
||||||
|
source.err = nil
|
||||||
|
if version, err := service.ObserveVersion(ctx, target); version != "" || !errors.Is(err, ErrConnection) {
|
||||||
|
t.Fatal("connection failure returned evidence")
|
||||||
|
}
|
||||||
|
connector.err = nil
|
||||||
|
if _, err := service.ObserveVersion(ctx, target); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
connector.databases[0].err = ErrObservation
|
||||||
|
if version, err := service.ObserveVersion(ctx, target); version != "" || !errors.Is(err, ErrObservation) {
|
||||||
|
t.Fatal("failed query returned evidence")
|
||||||
|
}
|
||||||
|
if connector.databases[0].closes != 1 {
|
||||||
|
t.Fatal("failed connection retained")
|
||||||
|
}
|
||||||
|
if _, err := service.ObserveVersion(ctx, target); err != nil {
|
||||||
|
t.Fatal("retry failed", err)
|
||||||
|
}
|
||||||
|
service.Close()
|
||||||
|
service.Close()
|
||||||
|
if connector.databases[1].closes != 1 {
|
||||||
|
t.Fatal("shutdown did not close once")
|
||||||
|
}
|
||||||
|
if _, err := service.ObserveVersion(ctx, target); !errors.Is(err, ErrClosed) {
|
||||||
|
t.Fatal("closed service accepted work")
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user