diff --git a/.custom-gcl.yml b/.custom-gcl.yml new file mode 100644 index 0000000..c162aba --- /dev/null +++ b/.custom-gcl.yml @@ -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 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a36f222 --- /dev/null +++ b/.dockerignore @@ -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 diff --git a/.gitea/workflows/verify.yml b/.gitea/workflows/verify.yml new file mode 100644 index 0000000..e7f1b3b --- /dev/null +++ b/.gitea/workflows/verify.yml @@ -0,0 +1,43 @@ +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 diff --git a/.gitignore b/.gitignore index 2ad5e90..74ff2b7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /bin/ /dist/ /coverage/ +/cover.out # Local configuration and credentials .env diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..26d7b1b --- /dev/null +++ b/.golangci.yml @@ -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$ diff --git a/AGENTS.md b/AGENTS.md index 6b33062..c40b995 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,6 +3,9 @@ - 本仓库是 ddupan.top homelab 的内部基础设施控制平面,不以通用发行版为初期目标。 - 提交、文档和代码注释优先使用中文;公共 API 标识符和代码遵循对应语言惯例。 - 不要重新实现已有成熟后端的核心能力;新增实现前先确认能否通过稳定 API 进行薄适配。 +- 在自行设计通用控制循环、资源生命周期、调度、回收或故障恢复机制前,先调查 Kubernetes + 核心及成熟开源 controller/operator 的实现;优先复用经过验证的模式,并记录有意偏离的 + 理由。 - 不要引入统一包装所有能力的 Application CRD;应用应直接组合正交的平台资源。 - 所有 controller 必须考虑幂等、observe、finalizer、conditions、删除策略和恢复行为。 - Secret、token、kubeconfig 及具体生产凭据不得提交到仓库。 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3c44459 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c570f9d --- /dev/null +++ b/Makefile @@ -0,0 +1,229 @@ +# 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\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: 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= 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=> 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 diff --git a/PROJECT b/PROJECT new file mode 100644 index 0000000..6c8c931 --- /dev/null +++ b/PROJECT @@ -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" diff --git a/api/execution/v1alpha1/groupversion_info.go b/api/execution/v1alpha1/groupversion_info.go new file mode 100644 index 0000000..f8b7abb --- /dev/null +++ b/api/execution/v1alpha1/groupversion_info.go @@ -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 +) diff --git a/api/execution/v1alpha1/job_types.go b/api/execution/v1alpha1/job_types.go new file mode 100644 index 0000000..297a05a --- /dev/null +++ b/api/execution/v1alpha1/job_types.go @@ -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 + }) +} diff --git a/api/execution/v1alpha1/jobclass_types.go b/api/execution/v1alpha1/jobclass_types.go new file mode 100644 index 0000000..e761f97 --- /dev/null +++ b/api/execution/v1alpha1/jobclass_types.go @@ -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 + }) +} diff --git a/api/execution/v1alpha1/kubernetesexecutionparameters_types.go b/api/execution/v1alpha1/kubernetesexecutionparameters_types.go new file mode 100644 index 0000000..42545a9 --- /dev/null +++ b/api/execution/v1alpha1/kubernetesexecutionparameters_types.go @@ -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 + }) +} diff --git a/api/execution/v1alpha1/opensandboxexecutionparameters_types.go b/api/execution/v1alpha1/opensandboxexecutionparameters_types.go new file mode 100644 index 0000000..faffd5c --- /dev/null +++ b/api/execution/v1alpha1/opensandboxexecutionparameters_types.go @@ -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 + }) +} diff --git a/api/execution/v1alpha1/validation_test.go b/api/execution/v1alpha1/validation_test.go new file mode 100644 index 0000000..1d3fcfe --- /dev/null +++ b/api/execution/v1alpha1/validation_test.go @@ -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"}, + }, + } +} diff --git a/api/execution/v1alpha1/zz_generated.deepcopy.go b/api/execution/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000..b5e5251 --- /dev/null +++ b/api/execution/v1alpha1/zz_generated.deepcopy.go @@ -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 +} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..d5bb38c --- /dev/null +++ b/cmd/main.go @@ -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/controller-runtime@v0.25.0/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/controller-runtime@v0.25.0/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) + } +} diff --git a/config/crd/bases/execution.ayatori.ddupan.top_jobclasses.yaml b/config/crd/bases/execution.ayatori.ddupan.top_jobclasses.yaml new file mode 100644 index 0000000..490bcbb --- /dev/null +++ b/config/crd/bases/execution.ayatori.ddupan.top_jobclasses.yaml @@ -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: {} diff --git a/config/crd/bases/execution.ayatori.ddupan.top_jobs.yaml b/config/crd/bases/execution.ayatori.ddupan.top_jobs.yaml new file mode 100644 index 0000000..f328f64 --- /dev/null +++ b/config/crd/bases/execution.ayatori.ddupan.top_jobs.yaml @@ -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: {} diff --git a/config/crd/bases/execution.ayatori.ddupan.top_kubernetesexecutionparameters.yaml b/config/crd/bases/execution.ayatori.ddupan.top_kubernetesexecutionparameters.yaml new file mode 100644 index 0000000..3e2c778 --- /dev/null +++ b/config/crd/bases/execution.ayatori.ddupan.top_kubernetesexecutionparameters.yaml @@ -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 using the matching 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 diff --git a/config/crd/bases/execution.ayatori.ddupan.top_opensandboxexecutionparameters.yaml b/config/crd/bases/execution.ayatori.ddupan.top_opensandboxexecutionparameters.yaml new file mode 100644 index 0000000..e771a34 --- /dev/null +++ b/config/crd/bases/execution.ayatori.ddupan.top_opensandboxexecutionparameters.yaml @@ -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 diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml new file mode 100644 index 0000000..aad1dec --- /dev/null +++ b/config/crd/kustomization.yaml @@ -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 diff --git a/config/crd/kustomizeconfig.yaml b/config/crd/kustomizeconfig.yaml new file mode 100644 index 0000000..61361ff --- /dev/null +++ b/config/crd/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 diff --git a/config/default/cert_metrics_manager_patch.yaml b/config/default/cert_metrics_manager_patch.yaml new file mode 100644 index 0000000..d975015 --- /dev/null +++ b/config/default/cert_metrics_manager_patch.yaml @@ -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 diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml new file mode 100644 index 0000000..2123976 --- /dev/null +++ b/config/default/kustomization.yaml @@ -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 diff --git a/config/default/manager_metrics_patch.yaml b/config/default/manager_metrics_patch.yaml new file mode 100644 index 0000000..2aaef65 --- /dev/null +++ b/config/default/manager_metrics_patch.yaml @@ -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 diff --git a/config/default/metrics_service.yaml b/config/default/metrics_service.yaml new file mode 100644 index 0000000..68eb600 --- /dev/null +++ b/config/default/metrics_service.yaml @@ -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 diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml new file mode 100644 index 0000000..5c5f0b8 --- /dev/null +++ b/config/manager/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- manager.yaml diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml new file mode 100644 index 0000000..e72e1a5 --- /dev/null +++ b/config/manager/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 diff --git a/config/network-policy/allow-metrics-traffic.yaml b/config/network-policy/allow-metrics-traffic.yaml new file mode 100644 index 0000000..ce6e5b3 --- /dev/null +++ b/config/network-policy/allow-metrics-traffic.yaml @@ -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 diff --git a/config/network-policy/kustomization.yaml b/config/network-policy/kustomization.yaml new file mode 100644 index 0000000..ec0fb5e --- /dev/null +++ b/config/network-policy/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- allow-metrics-traffic.yaml diff --git a/config/prometheus/kustomization.yaml b/config/prometheus/kustomization.yaml new file mode 100644 index 0000000..fdc5481 --- /dev/null +++ b/config/prometheus/kustomization.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 diff --git a/config/prometheus/monitor.yaml b/config/prometheus/monitor.yaml new file mode 100644 index 0000000..f16c930 --- /dev/null +++ b/config/prometheus/monitor.yaml @@ -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 diff --git a/config/prometheus/monitor_tls_patch.yaml b/config/prometheus/monitor_tls_patch.yaml new file mode 100644 index 0000000..5bf84ce --- /dev/null +++ b/config/prometheus/monitor_tls_patch.yaml @@ -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 diff --git a/config/rbac/execution_job_admin_role.yaml b/config/rbac/execution_job_admin_role.yaml new file mode 100644 index 0000000..c2bf01c --- /dev/null +++ b/config/rbac/execution_job_admin_role.yaml @@ -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 diff --git a/config/rbac/execution_job_editor_role.yaml b/config/rbac/execution_job_editor_role.yaml new file mode 100644 index 0000000..b2242e4 --- /dev/null +++ b/config/rbac/execution_job_editor_role.yaml @@ -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 diff --git a/config/rbac/execution_job_viewer_role.yaml b/config/rbac/execution_job_viewer_role.yaml new file mode 100644 index 0000000..c4ca78c --- /dev/null +++ b/config/rbac/execution_job_viewer_role.yaml @@ -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 diff --git a/config/rbac/execution_jobclass_admin_role.yaml b/config/rbac/execution_jobclass_admin_role.yaml new file mode 100644 index 0000000..a6f3fa0 --- /dev/null +++ b/config/rbac/execution_jobclass_admin_role.yaml @@ -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 diff --git a/config/rbac/execution_jobclass_editor_role.yaml b/config/rbac/execution_jobclass_editor_role.yaml new file mode 100644 index 0000000..a882d81 --- /dev/null +++ b/config/rbac/execution_jobclass_editor_role.yaml @@ -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 diff --git a/config/rbac/execution_jobclass_viewer_role.yaml b/config/rbac/execution_jobclass_viewer_role.yaml new file mode 100644 index 0000000..2bcbcd7 --- /dev/null +++ b/config/rbac/execution_jobclass_viewer_role.yaml @@ -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 diff --git a/config/rbac/execution_kubernetesexecutionparameters_admin_role.yaml b/config/rbac/execution_kubernetesexecutionparameters_admin_role.yaml new file mode 100644 index 0000000..1c36404 --- /dev/null +++ b/config/rbac/execution_kubernetesexecutionparameters_admin_role.yaml @@ -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 diff --git a/config/rbac/execution_kubernetesexecutionparameters_editor_role.yaml b/config/rbac/execution_kubernetesexecutionparameters_editor_role.yaml new file mode 100644 index 0000000..bc421f8 --- /dev/null +++ b/config/rbac/execution_kubernetesexecutionparameters_editor_role.yaml @@ -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 diff --git a/config/rbac/execution_kubernetesexecutionparameters_viewer_role.yaml b/config/rbac/execution_kubernetesexecutionparameters_viewer_role.yaml new file mode 100644 index 0000000..e1be8bf --- /dev/null +++ b/config/rbac/execution_kubernetesexecutionparameters_viewer_role.yaml @@ -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 diff --git a/config/rbac/execution_opensandboxexecutionparameters_admin_role.yaml b/config/rbac/execution_opensandboxexecutionparameters_admin_role.yaml new file mode 100644 index 0000000..9534f7d --- /dev/null +++ b/config/rbac/execution_opensandboxexecutionparameters_admin_role.yaml @@ -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 diff --git a/config/rbac/execution_opensandboxexecutionparameters_editor_role.yaml b/config/rbac/execution_opensandboxexecutionparameters_editor_role.yaml new file mode 100644 index 0000000..281366c --- /dev/null +++ b/config/rbac/execution_opensandboxexecutionparameters_editor_role.yaml @@ -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 diff --git a/config/rbac/execution_opensandboxexecutionparameters_viewer_role.yaml b/config/rbac/execution_opensandboxexecutionparameters_viewer_role.yaml new file mode 100644 index 0000000..282d16e --- /dev/null +++ b/config/rbac/execution_opensandboxexecutionparameters_viewer_role.yaml @@ -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 diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml new file mode 100644 index 0000000..71fc603 --- /dev/null +++ b/config/rbac/kustomization.yaml @@ -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 diff --git a/config/rbac/leader_election_role.yaml b/config/rbac/leader_election_role.yaml new file mode 100644 index 0000000..76dbba8 --- /dev/null +++ b/config/rbac/leader_election_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 diff --git a/config/rbac/leader_election_role_binding.yaml b/config/rbac/leader_election_role_binding.yaml new file mode 100644 index 0000000..b4d9b9c --- /dev/null +++ b/config/rbac/leader_election_role_binding.yaml @@ -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 diff --git a/config/rbac/metrics_auth_role.yaml b/config/rbac/metrics_auth_role.yaml new file mode 100644 index 0000000..32d2e4e --- /dev/null +++ b/config/rbac/metrics_auth_role.yaml @@ -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 diff --git a/config/rbac/metrics_auth_role_binding.yaml b/config/rbac/metrics_auth_role_binding.yaml new file mode 100644 index 0000000..e775d67 --- /dev/null +++ b/config/rbac/metrics_auth_role_binding.yaml @@ -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 diff --git a/config/rbac/metrics_reader_role.yaml b/config/rbac/metrics_reader_role.yaml new file mode 100644 index 0000000..51a75db --- /dev/null +++ b/config/rbac/metrics_reader_role.yaml @@ -0,0 +1,9 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: metrics-reader +rules: +- nonResourceURLs: + - "/metrics" + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml new file mode 100644 index 0000000..759ce82 --- /dev/null +++ b/config/rbac/role.yaml @@ -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"] diff --git a/config/rbac/role_binding.yaml b/config/rbac/role_binding.yaml new file mode 100644 index 0000000..ff58301 --- /dev/null +++ b/config/rbac/role_binding.yaml @@ -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 diff --git a/config/rbac/service_account.yaml b/config/rbac/service_account.yaml new file mode 100644 index 0000000..528247f --- /dev/null +++ b/config/rbac/service_account.yaml @@ -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 diff --git a/config/samples/execution_v1alpha1_job.yaml b/config/samples/execution_v1alpha1_job.yaml new file mode 100644 index 0000000..22f00d8 --- /dev/null +++ b/config/samples/execution_v1alpha1_job.yaml @@ -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 diff --git a/config/samples/execution_v1alpha1_jobclass.yaml b/config/samples/execution_v1alpha1_jobclass.yaml new file mode 100644 index 0000000..15ac7e1 --- /dev/null +++ b/config/samples/execution_v1alpha1_jobclass.yaml @@ -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 diff --git a/config/samples/execution_v1alpha1_kubernetesexecutionparameters.yaml b/config/samples/execution_v1alpha1_kubernetesexecutionparameters.yaml new file mode 100644 index 0000000..a18c10a --- /dev/null +++ b/config/samples/execution_v1alpha1_kubernetesexecutionparameters.yaml @@ -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 diff --git a/config/samples/execution_v1alpha1_opensandboxexecutionparameters.yaml b/config/samples/execution_v1alpha1_opensandboxexecutionparameters.yaml new file mode 100644 index 0000000..648a297 --- /dev/null +++ b/config/samples/execution_v1alpha1_opensandboxexecutionparameters.yaml @@ -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 diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml new file mode 100644 index 0000000..2d006c3 --- /dev/null +++ b/config/samples/kustomization.yaml @@ -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 diff --git a/docs/api/job-v1alpha1.md b/docs/api/job-v1alpha1.md new file mode 100644 index 0000000..d0a2414 --- /dev/null +++ b/docs/api/job-v1alpha1.md @@ -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) diff --git a/docs/api/jobclass-v1alpha1.md b/docs/api/jobclass-v1alpha1.md new file mode 100644 index 0000000..e64dde2 --- /dev/null +++ b/docs/api/jobclass-v1alpha1.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/) diff --git a/docs/decisions/0004-modular-controller-boundaries.md b/docs/decisions/0004-modular-controller-boundaries.md new file mode 100644 index 0000000..0072562 --- /dev/null +++ b/docs/decisions/0004-modular-controller-boundaries.md @@ -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,确认 + 它来自真实后端差异而非单一实现假设。 diff --git a/docs/decisions/0005-ephemeral-job-retention.md b/docs/decisions/0005-ephemeral-job-retention.md new file mode 100644 index 0000000..9698654 --- /dev/null +++ b/docs/decisions/0005-ephemeral-job-retention.md @@ -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) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6e7d4b7 --- /dev/null +++ b/go.mod @@ -0,0 +1,99 @@ +module git.ddupan.top/panxiao81/ayatori + +go 1.27.1 + +require ( + 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 + 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/inconshreveable/mousetrap v1.1.0 // indirect + github.com/json-iterator/go v1.1.12 // 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/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.4 // 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.40.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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7684ec1 --- /dev/null +++ b/go.sum @@ -0,0 +1,247 @@ +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +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/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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +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/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/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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +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 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +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.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +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.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +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.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +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/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +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.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= diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt new file mode 100644 index 0000000..e69de29 diff --git a/internal/execution/state.go b/internal/execution/state.go new file mode 100644 index 0000000..93eb4a4 --- /dev/null +++ b/internal/execution/state.go @@ -0,0 +1,94 @@ +package execution + +import ( + "fmt" + + executionv1alpha1 "git.ddupan.top/panxiao81/ayatori/api/execution/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type JobState string + +const ( + JobStateResolving JobState = "Resolving" + JobStateScheduling JobState = "Scheduling" + JobStateStarting JobState = "Starting" + JobStateRunning JobState = "Running" + JobStateCancelling JobState = "Cancelling" + JobStateResultUnknown JobState = "ResultUnknown" + JobStateSucceeded JobState = "Succeeded" + JobStateFailed JobState = "Failed" + JobStateCancelled JobState = "Cancelled" + JobStateDeleting JobState = "Deleting" +) + +// StateOf derives a presentation state from durable API facts. The derived +// state is deliberately not persisted as a second source of truth. +func StateOf(job *executionv1alpha1.Job) JobState { + if !job.DeletionTimestamp.IsZero() { + return JobStateDeleting + } + + succeeded := condition(job.Status.Conditions, executionv1alpha1.JobConditionSucceeded) + if succeeded != nil { + switch succeeded.Status { + case metav1.ConditionTrue: + return JobStateSucceeded + case metav1.ConditionFalse: + if succeeded.Reason == "Cancelled" { + return JobStateCancelled + } + return JobStateFailed + } + } + + if job.Spec.DesiredState == executionv1alpha1.JobDesiredStateCancelled { + return JobStateCancelling + } + if succeeded != nil && succeeded.Status == metav1.ConditionUnknown && succeeded.Reason == "ResultUnknown" { + return JobStateResultUnknown + } + if job.Status.StartTime != nil { + return JobStateRunning + } + if conditionTrue(job.Status.Conditions, executionv1alpha1.JobConditionScheduled) { + return JobStateStarting + } + if conditionTrue(job.Status.Conditions, executionv1alpha1.JobConditionAccepted) { + return JobStateScheduling + } + return JobStateResolving +} + +// ValidateStatus checks invariants that every adapter must preserve. +func ValidateStatus(job *executionv1alpha1.Job) error { + succeeded := condition(job.Status.Conditions, executionv1alpha1.JobConditionSucceeded) + terminal := succeeded != nil && (succeeded.Status == metav1.ConditionTrue || succeeded.Status == metav1.ConditionFalse) + if terminal && job.Status.CompletionTime == nil { + return fmt.Errorf("terminal job must have completionTime") + } + if job.Status.CompletionTime != nil && !terminal { + return fmt.Errorf("completionTime requires a terminal Succeeded condition") + } + if job.Status.StartTime != nil && !conditionTrue(job.Status.Conditions, executionv1alpha1.JobConditionScheduled) { + return fmt.Errorf("startTime requires Scheduled=True") + } + if job.Status.StartTime != nil && job.Status.CompletionTime != nil && job.Status.CompletionTime.Before(job.Status.StartTime) { + return fmt.Errorf("completionTime must not precede startTime") + } + return nil +} + +func condition(conditions []metav1.Condition, conditionType string) *metav1.Condition { + for i := range conditions { + if conditions[i].Type == conditionType { + return &conditions[i] + } + } + return nil +} + +func conditionTrue(conditions []metav1.Condition, conditionType string) bool { + current := condition(conditions, conditionType) + return current != nil && current.Status == metav1.ConditionTrue +} diff --git a/internal/execution/state_test.go b/internal/execution/state_test.go new file mode 100644 index 0000000..09566ac --- /dev/null +++ b/internal/execution/state_test.go @@ -0,0 +1,85 @@ +package execution + +import ( + "testing" + "time" + + executionv1alpha1 "git.ddupan.top/panxiao81/ayatori/api/execution/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestStateOf(t *testing.T) { + now := metav1.NewTime(time.Now()) + tests := []struct { + name string + job executionv1alpha1.Job + want JobState + }{ + {name: "resolving", want: JobStateResolving}, + {name: "scheduling", job: jobWithConditions(newCondition(executionv1alpha1.JobConditionAccepted, metav1.ConditionTrue, "Valid")), want: JobStateScheduling}, + {name: "starting", job: jobWithConditions(newCondition(executionv1alpha1.JobConditionScheduled, metav1.ConditionTrue, "BackendCreated")), want: JobStateStarting}, + {name: "running", job: executionv1alpha1.Job{Status: executionv1alpha1.JobStatus{StartTime: &now}}, want: JobStateRunning}, + {name: "result unknown", job: jobWithConditions(newCondition(executionv1alpha1.JobConditionSucceeded, metav1.ConditionUnknown, "ResultUnknown")), want: JobStateResultUnknown}, + {name: "cancelling", job: executionv1alpha1.Job{Spec: executionv1alpha1.JobSpec{DesiredState: executionv1alpha1.JobDesiredStateCancelled}}, want: JobStateCancelling}, + {name: "succeeded", job: jobWithConditions(newCondition(executionv1alpha1.JobConditionSucceeded, metav1.ConditionTrue, "Completed")), want: JobStateSucceeded}, + {name: "failed", job: jobWithConditions(newCondition(executionv1alpha1.JobConditionSucceeded, metav1.ConditionFalse, "ExitCode")), want: JobStateFailed}, + {name: "cancelled", job: jobWithConditions(newCondition(executionv1alpha1.JobConditionSucceeded, metav1.ConditionFalse, "Cancelled")), want: JobStateCancelled}, + {name: "terminal beats desired cancellation", job: func() executionv1alpha1.Job { + j := jobWithConditions(newCondition(executionv1alpha1.JobConditionSucceeded, metav1.ConditionTrue, "Completed")) + j.Spec.DesiredState = executionv1alpha1.JobDesiredStateCancelled + return j + }(), want: JobStateSucceeded}, + {name: "deleting beats terminal", job: func() executionv1alpha1.Job { + j := jobWithConditions(newCondition(executionv1alpha1.JobConditionSucceeded, metav1.ConditionTrue, "Completed")) + j.DeletionTimestamp = &now + return j + }(), want: JobStateDeleting}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := StateOf(&tt.job); got != tt.want { + t.Fatalf("StateOf() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestValidateStatus(t *testing.T) { + start := metav1.NewTime(time.Unix(100, 0)) + finish := metav1.NewTime(time.Unix(200, 0)) + earlier := metav1.NewTime(time.Unix(50, 0)) + scheduled := newCondition(executionv1alpha1.JobConditionScheduled, metav1.ConditionTrue, "BackendCreated") + succeeded := newCondition(executionv1alpha1.JobConditionSucceeded, metav1.ConditionTrue, "Completed") + + tests := []struct { + name string + status executionv1alpha1.JobStatus + wantErr bool + }{ + {name: "empty status"}, + {name: "running", status: executionv1alpha1.JobStatus{Conditions: []metav1.Condition{scheduled}, StartTime: &start}}, + {name: "completed", status: executionv1alpha1.JobStatus{Conditions: []metav1.Condition{scheduled, succeeded}, StartTime: &start, CompletionTime: &finish}}, + {name: "terminal without completion time", status: executionv1alpha1.JobStatus{Conditions: []metav1.Condition{succeeded}}, wantErr: true}, + {name: "completion without terminal", status: executionv1alpha1.JobStatus{CompletionTime: &finish}, wantErr: true}, + {name: "start without scheduling", status: executionv1alpha1.JobStatus{StartTime: &start}, wantErr: true}, + {name: "completion before start", status: executionv1alpha1.JobStatus{Conditions: []metav1.Condition{scheduled, succeeded}, StartTime: &start, CompletionTime: &earlier}, wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateStatus(&executionv1alpha1.Job{Status: tt.status}) + if (err != nil) != tt.wantErr { + t.Fatalf("ValidateStatus() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func newCondition(conditionType string, status metav1.ConditionStatus, reason string) metav1.Condition { + return metav1.Condition{Type: conditionType, Status: status, Reason: reason} +} + +func jobWithConditions(conditions ...metav1.Condition) executionv1alpha1.Job { + return executionv1alpha1.Job{Status: executionv1alpha1.JobStatus{Conditions: conditions}} +}